[
  {
    "path": ".cargo/config.toml",
    "content": "[env]\n# Use the project's venv Python for PyO3 builds\nPYO3_PYTHON = { value = \".venv/bin/python3\", relative = true }\n"
  },
  {
    "path": ".claude/settings.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(cargo:*)\",\n      \"Bash(timeout 30s cargo:*)\",\n      \"Bash(timeout 10s cargo:*)\",\n      \"Bash(timeout 15s cargo:*)\",\n      \"Bash(RUST_BACKTRACE=1 cargo:*)\",\n      \"Bash(make:*)\",\n      \"Bash(INLINE_SNAPSHOT_DEFAULT_FLAGS=disable make:*)\",\n      \"Bash(mkdir:*)\",\n      \"Bash(uv:*)\",\n      \"Bash(echo:*)\",\n      \"Bash(rg:*)\",\n      \"Bash(sed:*)\",\n      \"Bash(grep:*)\",\n      \"Bash(fastmod:*)\",\n      \"Bash(find:*)\",\n      \"Bash(ls:*)\",\n      \"Bash(cat:*)\",\n      \"Bash(python3:*)\",\n      \"Bash(git show:*)\",\n      \"Bash(git diff:*)\",\n      \"Bash(git add:*)\",\n      \"Bash(git stash:*)\",\n      \"Bash(git checkout:*)\",\n      \"Bash(git mv:*)\",\n      \"Bash(git log:*)\",\n      \"Bash(git grep:*)\",\n      \"Bash(git status:*)\",\n      \"Bash(gh pr view:*)\",\n      \"Bash(gh api:*)\",\n      \"Bash(gh run view:*)\",\n      \"Bash(gh issue view:*)\",\n      \"Skill(python-playground)\",\n      \"Skill(fastmod)\",\n      \"Skill(coverage)\",\n      \"Bash(wc:*)\",\n      \"Bash(xxd:*)\",\n      \"Bash(head:*)\",\n      \"Bash(rustup show:*)\",\n      \"Bash(xargs:*)\",\n      \"Bash(perl:*)\",\n      \"Bash(du:*)\",\n      \"Bash(npm run:*)\",\n      \"Bash(mkdir:*)\",\n      \"WebSearch\",\n      \"WebFetch(domain:pypi.org)\",\n      \"WebFetch(domain:docs.anthropic.com)\",\n      \"WebFetch(domain:github.com)\",\n      \"WebFetch(domain:15r10nk.github.io)\",\n      \"WebFetch(domain:docs.rs)\",\n      \"WebFetch(domain:pyo3.rs)\",\n      \"WebFetch(domain:napi.rs)\",\n      \"WebFetch(domain:app.codecov.io)\"\n    ],\n    \"deny\": [],\n    \"ask\": []\n  }\n}\n"
  },
  {
    "path": ".claude/skills/coverage/SKILL.md",
    "content": "---\nname: coverage\ndescription: Fetch coverage diff from Codecov for the current branch or a specific PR. Shows uncovered lines, patch coverage, and overall coverage change.\n---\n\n# Coverage\n\nFetch line-by-line coverage information from Codecov for a GitHub pull request.\n\n## Instructions\n\nUse this skill to check code coverage for your changes before merging.\n\n### Current branch (auto-detect PR)\n\n```bash\nuv run scripts/codecov_diff.py\n```\n\nThis auto-detects the org, repo, and PR number using the `gh` CLI based on the current branch.\n\n### Specific PR number\n\n```bash\nuv run scripts/codecov_diff.py 123\n```\n\n## Output\n\nThe script outputs:\n- PR title and state\n- HEAD coverage (overall coverage on the branch)\n- Patch coverage (coverage of changed lines only)\n- Coverage change (+/- percentage)\n- Per-file breakdown with:\n  - Missed line count\n  - Patch coverage percentage\n  - Specific uncovered line numbers (as ranges like `45-48, 52, 60-65`)\n  - Partial coverage line numbers\n\n## Requirements\n\n- The `gh` CLI must be installed and authenticated for auto-detection\n- The PR must have Codecov coverage data uploaded\n"
  },
  {
    "path": ".claude/skills/fastmod/SKILL.md",
    "content": "---\nname: fastmod\ndescription: Use fastmod to make mass code updates to avoid many repetitive changes.\n---\n\n# fastmod\n\n## Instructions\n\nYou can occasionally use `fastmod` or `sed` to make mass updates to the codebase and avoid wasting tokens changing each case one at a time.\n\nBefore making many repetitive changes to the codebase, consider using `fastmod --accept-all`.\n\nTHINK HARD about how best to use `fastmod` as it can dramatically improve your productivity.\n\n## Examples\n\nExample of switching the `py_type` function to use `impl ResourceTracker` instead of `T: ResourceTracker`:\n\n```bash\nfastmod --accept-all 'fn py_type<T: ResourceTracker>(\\(.+?)<T>' 'fn py_type$1<impl ResourceTracker>'\n```\n"
  },
  {
    "path": ".claude/skills/python-playground/SKILL.md",
    "content": "---\nname: python-playground\ndescription: Run and test Python code in a dedicated playground directory. Use when you need to execute Python scripts, test code snippets, investigate CPython behavior, or experiment with Python without affecting the main codebase.\n---\n\n# Python Playground\n\nRun Python code in an isolated playground directory for testing and experimentation.\n\n## Instructions\n\n1. First, ensure the playground directory exists: `mkdir -p playground`\n2. Use the Write tool to create the Python file at `playground/test.py`\n3. Run with: `uv run playground/test.py` to test cpython behavior or `cargo run -- playground/test.py` to test monty behavior\n\nIMPORTANT: Use separate tool calls for each step - do NOT chain commands with `&&` or use heredocs. This allows the pre-approved commands to work without prompting.\n\n## Example workflow\n\nStep 1 - Create directory (Bash, already allowed):\n```bash\nmkdir -p playground\n```\n\nStep 2 - Write code (use Write tool, not cat):\nWrite to `playground/test.py`:\n```python\ndef foo():\n    raise ValueError('test')\n\nfoo()\n```\n\nStep 3 - Run script (Bash, already allowed):\n```bash\nuv run playground/test.py\n```\n\n## Guidelines\n\n- The `playground/` directory is gitignored\n- Use a different file name for each test you want to run, give the files recognizable names like `test_value_error.py`\n- Use `uv run ...` to run scripts (uses project Python)\n- Or, `cargo run -- ...` to run scripts using Monty\n- Use Write tool for creating files (avoids permission prompts)\n- Run mkdir and uv as separate commands (not chained)\n- do NOT delete files from playground after you've finished testing\n"
  },
  {
    "path": ".codecov.yml",
    "content": "codecov:\n  require_ci_to_pass: false\n\ncoverage:\n  precision: 2\n  # range: [90, 100]\n  status:\n    patch: false\n    project: false\n\ncomment:\n  layout: 'header, diff, flags, files, footer'\n"
  },
  {
    "path": ".github/actions/build-pgo-wheel/action.yml",
    "content": "name: Build PGO wheel\ndescription: Builds a PGO-optimized wheel for pydantic-monty\n\ninputs:\n  interpreter:\n    description: 'Interpreter(s) to build the wheel for'\n    required: true\n  rust-toolchain:\n    description: 'Rust toolchain to use'\n    required: true\n\noutputs:\n  wheel-dir:\n    description: 'Path to the directory containing built wheels'\n    value: ${{ steps.find_wheel.outputs.dir }}\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: prepare profiling directory\n      shell: bash\n      run: mkdir -p ${{ github.workspace }}/profdata\n\n    - name: build initial wheel (instrumented)\n      uses: PyO3/maturin-action@v1\n      with:\n        manylinux: auto\n        args: >\n          --release\n          --out pgo-wheel\n          --interpreter 3.12\n        rust-toolchain: ${{ inputs.rust-toolchain }}\n        docker-options: -e CI\n        working-directory: crates/monty-python\n      env:\n        RUSTFLAGS: '-Cprofile-generate=${{ github.workspace }}/profdata'\n\n    - name: detect rust host\n      run: echo RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) >> \"$GITHUB_ENV\"\n      shell: bash\n\n    - name: generate pgo data\n      run: |\n        pip install pydantic-monty --no-index --no-deps --find-links pgo-wheel --force-reinstall\n        python exercise.py\n        rustup run ${{ inputs.rust-toolchain }} bash -c 'echo LLVM_PROFDATA=$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/$RUST_HOST/bin/llvm-profdata >> \"$GITHUB_ENV\"'\n      shell: bash\n      working-directory: crates/monty-python\n\n    - name: merge pgo data\n      # PowerShell handles paths on Windows better, and works well enough on Unix\n      run: ${{ env.LLVM_PROFDATA }} merge -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata\n      shell: pwsh\n\n    - name: build pgo-optimized wheel\n      uses: PyO3/maturin-action@v1\n      with:\n        manylinux: auto\n        args: >\n          --release\n          --out dist\n          --interpreter ${{ inputs.interpreter }}\n        rust-toolchain: ${{ inputs.rust-toolchain }}\n        docker-options: -e CI\n        working-directory: crates/monty-python\n      env:\n        RUSTFLAGS: '-Cprofile-use=${{ github.workspace }}/merged.profdata'\n\n    - name: find built wheels\n      id: find_wheel\n      run: echo \"dir=crates/monty-python/dist\" >> \"$GITHUB_OUTPUT\"\n      shell: bash\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - \"**\"\n  pull_request: {}\n  workflow_dispatch:\n    inputs:\n      run_release:\n        description: \"Run release jobs (for manual tag releases)\"\n        type: boolean\n        default: false\n\nenv:\n  COLUMNS: 150\n  UV_PYTHON: \"3.14\"\n  UV_FROZEN: \"1\"\n  DEBUG: \"napi:*\"\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@nightly\n        with:\n          components: rustfmt, clippy\n      - uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: true\n          prefix-key: \"v1-rust\"\n\n      - uses: astral-sh/setup-uv@v7\n      - run: uv sync --all-packages --only-dev\n\n      - uses: actions/cache@v5\n        with:\n          path: ~/.cache/pre-commit\n          key: pre-commit|${{ env.UV_PYTHON }}|${{ hashFiles('.pre-commit-config.yaml') }}\n\n      - name: Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: npm\n          cache-dependency-path: crates/monty-js/package-lock.json\n\n      - name: Install dependencies\n        run: npm install\n        working-directory: crates/monty-js\n\n      - run: uvx pre-commit run --color=always --all-files --verbose\n        env:\n          SKIP: no-commit-to-branch\n\n      - name: Show diff on failure\n        if: failure()\n        run: git diff\n\n  test-rust:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          components: llvm-tools\n      - uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: true\n\n      - uses: taiki-e/install-action@cargo-llvm-cov\n\n      # this means pyo3 will use Python 3.14 in tests\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.14\"\n\n      - run: rustc --version --verbose\n      - run: python3 -V\n        # don't use .venv python in CI\n      - run: rm .cargo/config.toml\n\n      # coverage for `make test-no-features`\n      - run: cargo llvm-cov --no-report -p monty\n      # coverage for `make test-ref-count-panic`\n      - run: cargo llvm-cov --no-report -p monty --features ref-count-panic\n      # coverage for `make test-ref-count-return`\n      - run: cargo llvm-cov --no-report -p monty --features ref-count-return\n      # coverage for `make test-type-checking`\n      - run: cargo llvm-cov --no-report -p monty_type_checking -p monty_typeshed\n      # Generating text report:\n      - run: cargo llvm-cov report --ignore-filename-regex '(tests/|test_cases/|/tests\\.rs$$)'\n      # Generate codecov report (use `report` subcommand to avoid recompilation)\n      - run: cargo llvm-cov report --codecov --output-path=codecov.json --ignore-filename-regex '(tests/|test_cases/|/tests\\.rs$$)'\n\n      - uses: codecov/codecov-action@v5\n        with:\n          files: codecov.json\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n  test-python:\n    name: test python ${{ matrix.python-version }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    env:\n      UV_PYTHON: ${{ matrix.python-version }}\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n      - uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: true\n      - uses: astral-sh/setup-uv@v7\n\n      - run: uv sync --all-packages --only-dev\n      - run: make dev-py\n      - run: make pytest\n        # also test with a release build\n      - run: make dev-py-release\n      - run: make pytest\n        # test uv run exercise script\n      - run: uv run crates/monty-python/exercise.py\n\n  bench-test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@stable\n      - uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: true\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.14\"\n          # don't use .venv python in CI\n      - run: rm .cargo/config.toml\n\n      - run: make dev-bench\n\n  fuzz:\n    name: fuzz ${{ matrix.target }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        target:\n          - tokens_input_panic\n          # disable until https://github.com/astral-sh/ruff/issues/23198 is fixed\n          # - string_input_panic\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@nightly\n      - id: cache-rust\n        uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: true\n          prefix-key: \"v1-rust-fuzz\"\n          workspaces: \"crates/fuzz -> target\"\n\n      - if: steps.cache-rust.outputs.cache-hit != 'true'\n        run: cargo install cargo-fuzz\n\n      # don't use .venv python in CI\n      - run: rm .cargo/config.toml\n\n      - name: Run ${{ matrix.target }} fuzzer\n        run: |\n          # Use --sanitizer none to avoid ASAN/SanitizerCoverage linking issues on CI\n          # (undefined __sancov_gen_.* symbols). For short CI runs, we're mainly\n          # catching panics, not memory bugs.\n          cargo fuzz run --fuzz-dir crates/fuzz --sanitizer none ${{ matrix.target }} -- -max_total_time=60\n\n  # https://github.com/marketplace/actions/alls-green#why used for branch protection checks\n  check:\n    if: always()\n    needs:\n      - lint\n      - test-rust\n      - test-python\n      - bench-test\n      - fuzz\n    runs-on: ubuntu-latest\n    steps:\n      - name: Decide whether the needed jobs succeeded or failed\n        uses: re-actors/alls-green@release/v1\n        with:\n          jobs: ${{ toJSON(needs) }}\n\n  # Build source distribution\n  build-sdist:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n      - uses: PyO3/maturin-action@v1\n        with:\n          command: sdist\n          args: --out dist\n          rust-toolchain: stable\n          working-directory: crates/monty-python\n      - uses: actions/upload-artifact@v6\n        with:\n          name: pypi_files-sdist\n          path: crates/monty-python/dist\n\n  # Build wheels for exotic architectures (non-PGO)\n  build:\n    name: build on ${{ matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }})\n    # only run on push to main, on tags, or if 'Full Build' label is present\n    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Full Build') || (github.event_name == 'workflow_dispatch' && inputs.run_release)\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          # Linux aarch64\n          - os: linux\n            target: aarch64\n          # Linux i686\n          - os: linux\n            target: i686\n          # Linux armv7\n          - os: linux\n            target: armv7\n          # Linux ppc64le\n          - os: linux\n            target: ppc64le\n          # Linux s390x\n          - os: linux\n            target: s390x\n          # Linux x86_64 musl\n          - os: linux\n            target: x86_64\n            manylinux: musllinux_1_1\n          # Linux aarch64 musl\n          - os: linux\n            target: aarch64\n            manylinux: musllinux_1_1\n          # macOS x86_64 (Intel)\n          - os: macos\n            target: x86_64\n          # Windows i686\n          - os: windows\n            target: i686\n\n    runs-on: ${{ (matrix.os == 'linux' && 'ubuntu-latest') || (matrix.os == 'macos' && 'macos-latest') || (matrix.os == 'windows' && 'windows-latest') }}\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n\n      - name: build wheels\n        uses: PyO3/maturin-action@v1\n        with:\n          target: ${{ matrix.target }}\n          manylinux: ${{ matrix.manylinux || 'auto' }}\n          args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14\n          rust-toolchain: stable\n          docker-options: -e CI\n          working-directory: crates/monty-python\n\n      - uses: actions/upload-artifact@v6\n        with:\n          name: pypi_files-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'manylinux' }}\n          path: crates/monty-python/dist\n\n  # PGO-optimized builds for main platforms\n  build-pgo:\n    name: build pgo on ${{ matrix.os }}\n    # only run on push to main, on tags, or if 'Full Build' label is present\n    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Full Build') || (github.event_name == 'workflow_dispatch' && inputs.run_release)\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          # Linux x86_64 (manylinux)\n          - os: linux\n            runs-on: ubuntu-latest\n            interpreter: 3.10 3.11 3.12 3.13 3.14\n          # Windows x86_64\n          - os: windows\n            runs-on: windows-latest\n            interpreter: 3.10 3.11 3.12 3.13 3.14\n          # macOS aarch64 (Apple Silicon)\n          - os: macos\n            runs-on: macos-latest\n            interpreter: 3.10 3.11 3.12 3.13 3.14\n\n    runs-on: ${{ matrix.runs-on }}\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          components: llvm-tools\n\n      - name: build PGO wheel\n        id: pgo\n        uses: ./.github/actions/build-pgo-wheel\n        with:\n          interpreter: ${{ matrix.interpreter }}\n          rust-toolchain: stable\n\n      - uses: actions/upload-artifact@v6\n        with:\n          name: pypi_files-${{ matrix.os }}-pgo\n          path: ${{ steps.pgo.outputs.wheel-dir }}\n\n  # Test wheels on exotic architectures via QEMU\n  test-builds-arch:\n    name: test build on ${{ matrix.target }}\n    needs: [build]\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        target: [aarch64, armv7, s390x, ppc64le]\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/download-artifact@v7\n        with:\n          pattern: pypi_files-linux-${{ matrix.target }}-*\n          merge-multiple: true\n          path: dist\n\n      - uses: uraimo/run-on-arch-action@v3\n        name: install & test\n        with:\n          arch: ${{ matrix.target }}\n          distro: ubuntu22.04\n          dockerRunArgs: --volume \"${{ github.workspace }}/dist:/dist\"\n          install: |\n            apt-get update\n            apt-get install -y --no-install-recommends python3 python3-pip python3-venv\n          run: |\n            ls -lh /dist/\n            python3 -m venv venv\n            source venv/bin/activate\n            python3 -m pip install pydantic-monty --no-index --no-deps --find-links /dist --force-reinstall\n            python3 -c \"import pydantic_monty; print(pydantic_monty.Monty('1 + 2').run())\"\n\n  # Test wheels on main OS platforms\n  test-builds-os:\n    name: test build on ${{ matrix.os }}\n    needs: [build, build-pgo]\n    runs-on: ${{ matrix.runs-on }}\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: linux\n            runs-on: ubuntu-latest\n          - os: macos\n            runs-on: macos-latest\n          - os: windows\n            runs-on: windows-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n\n      - uses: actions/download-artifact@v7\n        with:\n          pattern: pypi_files-${{ matrix.os }}-*\n          merge-multiple: true\n          path: dist\n\n      - run: pip install pydantic-monty --no-index --find-links dist --force-reinstall\n      - run: python -c \"import pydantic_monty; print(pydantic_monty.Monty('1 + 2').run())\"\n\n  # Inspect built artifacts\n  inspect-python-assets:\n    needs: [build, build-pgo, build-sdist]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/download-artifact@v7\n        with:\n          pattern: pypi_files-*\n          merge-multiple: true\n          path: dist\n\n      - name: list files\n        run: |\n          ls -lhR dist/\n          ls -1 dist/ | wc -l\n          echo \"Expected ~50 wheel files (5 Python versions × 10 platform variants)\"\n\n      - uses: astral-sh/setup-uv@v7\n      - run: uvx twine check dist/*\n\n  # Release to PyPI\n  release-python:\n    name: release to PyPI\n    needs: [check, inspect-python-assets, test-builds-arch, test-builds-os]\n    if: success() && (startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.run_release))\n    runs-on: ubuntu-latest\n\n    environment:\n      name: release-python\n      url: https://pypi.org/project/pydantic-monty/${{ steps.check-version.outputs.VERSION }}\n\n    permissions:\n      id-token: write\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: true\n\n      - uses: actions/download-artifact@v7\n        with:\n          pattern: pypi_files-*\n          merge-multiple: true\n          path: dist\n\n      - id: check-version\n        uses: samuelcolvin/check-python-version@v5\n        with:\n          version_file_path: \"Cargo.toml\"\n\n      - run: ls -lhR dist/\n\n      - name: Publish to PyPI\n        run: \"uv publish --trusted-publishing always dist/*\"\n\n  build-js:\n    name: build JS - ${{ matrix.settings.target }} - node@22\n    runs-on: ${{ matrix.settings.host }}\n    # only run on push to main, on tags, or if 'Full Build' label is present\n    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Full Build') || (github.event_name == 'workflow_dispatch' && inputs.run_release)\n\n    strategy:\n      fail-fast: false\n      matrix:\n        settings:\n          - host: macos-latest\n            target: x86_64-apple-darwin\n            build: npm run build:napi -- --target x86_64-apple-darwin && npm run build:ts\n          - host: windows-latest\n            target: x86_64-pc-windows-msvc\n            build: npm run build:napi -- --target x86_64-pc-windows-msvc && npm run build:ts\n          - host: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            build: npm run build:napi -- --target x86_64-unknown-linux-gnu --use-napi-cross && npm run build:ts\n          - host: macos-latest\n            target: aarch64-apple-darwin\n            build: npm run build:napi -- --target aarch64-apple-darwin && npm run build:ts\n          - host: ubuntu-24.04-arm\n            target: aarch64-unknown-linux-gnu\n            build: npm run build:napi -- --target aarch64-unknown-linux-gnu && npm run build:ts\n          - host: ubuntu-latest\n            target: wasm32-wasip1-threads\n            build: npm run build:napi -- --target wasm32-wasip1-threads && npm run build:ts\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: npm\n          cache-dependency-path: crates/monty-js/package-lock.json\n      - name: Install\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          targets: ${{ matrix.settings.target }}\n      - name: Cache cargo\n        uses: actions/cache@v5\n        with:\n          path: |\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            ~/.napi-rs\n            .cargo-cache\n            target/\n          key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}\n      # don't use .venv python in CI\n      - run: rm .cargo/config.toml\n      - uses: mlugg/setup-zig@v2\n        if: ${{ contains(matrix.settings.target, 'musl') }}\n        with:\n          version: 0.14.1\n      - name: Install cargo-zigbuild\n        uses: taiki-e/install-action@v2\n        if: ${{ contains(matrix.settings.target, 'musl') }}\n        with:\n          tool: cargo-zigbuild\n      - name: Setup toolchain\n        run: ${{ matrix.settings.setup }}\n        if: ${{ matrix.settings.setup }}\n        shell: bash\n      - name: Install dependencies\n        run: npm install\n        working-directory: crates/monty-js\n      - name: Build\n        run: ${{ matrix.settings.build }}\n        shell: bash\n        working-directory: crates/monty-js\n      - name: Upload artifact\n        uses: actions/upload-artifact@v6\n        with:\n          name: js-bindings-${{ matrix.settings.target }}\n          path: |\n            crates/monty-js/monty.*.node\n            crates/monty-js/monty.*.wasm\n          if-no-files-found: error\n      # need to upload the .js files generated by napi; they are identical for whatever target\n      # so might as well upload from the linux job\n      - if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }}\n        name: Upload artifact\n        uses: actions/upload-artifact@v6\n        with:\n          name: js-stubs\n          path: |\n            crates/monty-js/browser.js\n            crates/monty-js/index.js\n            crates/monty-js/index.d.ts\n            crates/monty-js/wrapper.js\n            crates/monty-js/wrapper.d.ts\n            crates/monty-js/monty.wasi.cjs\n            crates/monty-js/monty.wasi-browser.js\n            crates/monty-js/wasi-worker.mjs\n            crates/monty-js/wasi-worker-browser.mjs\n          if-no-files-found: error\n    env:\n      MACOSX_DEPLOYMENT_TARGET: \"10.13\"\n      CARGO_INCREMENTAL: \"1\"\n\n  test-js-macOS-windows-binding:\n    name: Test JS bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}\n    needs:\n      - build-js\n    strategy:\n      fail-fast: false\n      matrix:\n        settings:\n          - host: windows-latest\n            target: x86_64-pc-windows-msvc\n            architecture: x64\n          - host: macos-latest\n            target: aarch64-apple-darwin\n            architecture: arm64\n          - host: macos-latest\n            target: x86_64-apple-darwin\n            architecture: x64\n        node:\n          - \"20\"\n          - \"22\"\n    runs-on: ${{ matrix.settings.host }}\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node }}\n          cache: npm\n          cache-dependency-path: crates/monty-js/package-lock.json\n          architecture: ${{ matrix.settings.architecture }}\n      - name: Install dependencies\n        run: npm install\n        working-directory: crates/monty-js\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-bindings-${{ matrix.settings.target }}\n          path: crates/monty-js\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-stubs\n          path: crates/monty-js\n      - name: List packages\n        run: ls -R .\n        shell: bash\n        working-directory: crates/monty-js\n      - name: Test bindings\n        run: npm test\n        working-directory: crates/monty-js\n\n  test-js-linux-binding:\n    name: Test JS ${{ matrix.target }} - node@${{ matrix.node }}\n    needs:\n      - build-js\n    strategy:\n      fail-fast: false\n      matrix:\n        target:\n          - x86_64-unknown-linux-gnu\n          - aarch64-unknown-linux-gnu\n        node:\n          - \"20\"\n          - \"22\"\n    runs-on: ${{ contains(matrix.target, 'aarch64') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node }}\n          cache: npm\n          cache-dependency-path: crates/monty-js/package-lock.json\n      - name: Output docker params\n        id: docker\n        run: |\n          node -e \"\n            if ('${{ matrix.target }}'.startsWith('aarch64')) {\n              console.log('PLATFORM=linux/arm64')\n            } else if ('${{ matrix.target }}'.startsWith('armv7')) {\n              console.log('PLATFORM=linux/arm/v7')\n            } else {\n              console.log('PLATFORM=linux/amd64')\n            }\n          \" >> $GITHUB_OUTPUT\n          node -e \"\n            if ('${{ matrix.target }}'.endsWith('-musl')) {\n              console.log('IMAGE=node:${{ matrix.node }}-alpine')\n            } else {\n              console.log('IMAGE=node:${{ matrix.node }}-slim')\n            }\n          \" >> $GITHUB_OUTPUT\n      - name: Install dependencies\n        run: npm install\n        working-directory: crates/monty-js\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-bindings-${{ matrix.target }}\n          path: crates/monty-js\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-stubs\n          path: crates/monty-js\n      - name: List packages\n        run: ls -R .\n        shell: bash\n        working-directory: crates/monty-js\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n        if: ${{ contains(matrix.target, 'armv7') }}\n        with:\n          platforms: all\n      - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes\n        if: ${{ contains(matrix.target, 'armv7') }}\n      - name: Test bindings\n        run: >\n          docker run --rm -v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }}/crates/monty-js --platform ${{ steps.docker.outputs.PLATFORM }} ${{ steps.docker.outputs.IMAGE }} npm test\n\n  test-js-wasi:\n    name: Test WASI target\n    needs:\n      - build-js\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: npm\n          cache-dependency-path: crates/monty-js/package-lock.json\n      - name: Install dependencies\n        run: npm install --cpu wasm32\n        working-directory: crates/monty-js\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-bindings-wasm32-wasip1-threads\n          path: crates/monty-js\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-stubs\n          path: crates/monty-js\n      - name: List packages\n        run: ls -R .\n        shell: bash\n        working-directory: crates/monty-js\n      - name: Test bindings\n        run: npm test\n        env:\n          NAPI_RS_FORCE_WASI: 1\n        working-directory: crates/monty-js\n\n  release-js:\n    name: Release to NPM\n    runs-on: ubuntu-latest\n    needs:\n      - check\n      - inspect-python-assets\n      - test-js-macOS-windows-binding\n      - test-js-linux-binding\n      - test-js-wasi\n    if: success() && (startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.run_release))\n    permissions:\n      contents: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v6\n      - name: Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: npm\n          cache-dependency-path: crates/monty-js/package-lock.json\n          registry-url: \"https://registry.npmjs.org\"\n      - name: Install dependencies\n        run: npm install\n        working-directory: crates/monty-js\n      - name: create npm dirs\n        run: npm run create-npm-dirs\n        working-directory: crates/monty-js\n      - name: Download all artifacts\n        uses: actions/download-artifact@v7\n        with:\n          pattern: js-bindings-*\n          path: crates/monty-js/artifacts\n          merge-multiple: true\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: js-stubs\n          path: crates/monty-js\n      - name: Move artifacts\n        run: npm run artifacts\n        working-directory: crates/monty-js\n      - name: List packages\n        run: ls -R ./npm\n        shell: bash\n        working-directory: crates/monty-js\n      - name: Publish\n        run: |\n          if [[ \"$GITHUB_REF\" =~ ^refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then\n            echo \"Publishing stable release\"\n            npm publish --provenance --access public\n          else\n            echo \"Publishing pre-release with 'next' tag\"\n            npm publish --provenance --tag next --access public\n          fi\n        working-directory: crates/monty-js\n"
  },
  {
    "path": ".github/workflows/codspeed.yml",
    "content": "name: CodSpeed\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  id-token: write\n\njobs:\n  benchmarks:\n    name: Run benchmarks\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@stable\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: true\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.14\"\n\n      - name: Remove .cargo config to use system Python\n        run: rm .cargo/config.toml\n\n      - name: Install cargo-codspeed\n        run: cargo install cargo-codspeed\n\n      - name: Build benchmarks\n        run: cargo codspeed build -p monty --bench main\n\n      - name: Run benchmarks\n        uses: CodSpeedHQ/action@v4\n        with:\n          mode: simulation\n          run: cargo codspeed run -p monty --bench main\n"
  },
  {
    "path": ".github/workflows/init-npm-packages.yml",
    "content": "name: Initialize NPM Platform Packages\n\n# Creates placeholder packages on npm for any new napi platform targets.\n# npm requires packages to exist before OIDC/provenance publishing can work,\n# so this must be run once per new target before the first release that includes it.\n#\n# After running, configure Trusted Publishing on npmjs.com for each new package:\n#   Package Settings > Trusted Publisher > GitHub Actions > pydantic/monty\n\non:\n  workflow_dispatch: {}\n\njobs:\n  init-packages:\n    name: Initialize missing platform packages\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: npm install\n        working-directory: crates/monty-js\n\n      - name: Create npm dirs\n        run: npm run create-npm-dirs\n        working-directory: crates/monty-js\n\n      - name: Check and create missing platform packages\n        working-directory: crates/monty-js\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n        run: |\n          created=()\n          for pkg_dir in npm/*/; do\n            name=$(node -p \"require('./${pkg_dir}package.json').name\")\n\n            if npm view \"$name\" version > /dev/null 2>&1; then\n              echo \"✓ $name already exists\"\n              continue\n            fi\n\n            echo \"Creating placeholder for $name...\"\n            tmp=$(mktemp -d)\n            node -e \"\n              const pkg = require('./${pkg_dir}package.json');\n              const placeholder = {\n                name: pkg.name,\n                version: '0.0.0',\n                description: 'Placeholder for OIDC trusted publishing setup — this version contains no code.',\n                license: pkg.license,\n                repository: pkg.repository,\n                publishConfig: pkg.publishConfig,\n                os: pkg.os,\n                cpu: pkg.cpu,\n              };\n              if (pkg.libc) placeholder.libc = pkg.libc;\n              require('fs').writeFileSync('${tmp}/package.json', JSON.stringify(placeholder, null, 2));\n            \"\n            (cd \"$tmp\" && npm publish --access public)\n            rm -rf \"$tmp\"\n            created+=(\"$name\")\n            echo \"✓ Created $name@0.0.0\"\n          done\n\n          echo \"\"\n          if [ ${#created[@]} -eq 0 ]; then\n            echo \"All platform packages already exist on npm.\"\n          else\n            echo \"Created ${#created[@]} new package(s):\"\n            for name in \"${created[@]}\"; do\n              echo \"  - $name\"\n              echo \"    Configure Trusted Publishing: https://www.npmjs.com/package/${name}/access\"\n            done\n            echo \"\"\n            echo \"⚠ You must configure Trusted Publishing for each new package on npmjs.com\"\n            echo \"  before the next release will succeed with OIDC/provenance.\"\n          fi\n"
  },
  {
    "path": ".gitignore",
    "content": "*.py[cod]\n*.so\n/.idea/\n/target/\n/env*/\n/*.py\n/TODO.md\n/monty\n/.claude/settings.local.json\n/scratch/\n/worktrees/\n/flame/\n/playground/\n/type-sizes.txt\n/sandbox/\n/starlark-rust/\n/.playwright-mcp/\n/crates/fuzz/artifacts/\n/crates/fuzz/corpus/\n/.worktrees/\n/.zed/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "fail_fast: true\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.0.1\n    hooks:\n      - id: no-commit-to-branch # prevent direct commits to the `main` branch\n      - id: check-yaml\n      - id: check-toml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n      - id: check-added-large-files\n\n  - repo: https://github.com/codespell-project/codespell\n    # Configuration for codespell is in pyproject.toml\n    rev: v2.3.0\n    hooks:\n      - id: codespell\n        additional_dependencies:\n          - tomli\n\n  - repo: local\n    hooks:\n      - id: format-rs\n        name: Format Rust\n        entry: make format-rs\n        types: [rust]\n        language: system\n        pass_filenames: false\n      - id: lint-rs\n        name: Lint Rust\n        entry: make lint-rs\n        types: [rust]\n        language: system\n        pass_filenames: false\n      - id: format-lint-py\n        name: Format & Lint Python\n        entry: make format-lint-py\n        types: [python]\n        language: system\n        pass_filenames: false\n      - id: format-lint-js\n        name: Format & Lint TypeScript\n        entry: make format-js lint-js\n        types: [ts]\n        language: system\n        pass_filenames: false\n"
  },
  {
    "path": ".python-version",
    "content": "3.14\n"
  },
  {
    "path": ".rustfmt.toml",
    "content": "max_width = 120\nimports_granularity = \"Crate\"\ngroup_imports = \"StdExternalCrate\"\nreorder_imports = true\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nMonty is a sandboxed Python interpreter written in Rust. It parses Python code using Ruff's `ruff_python_parser` but implements its own runtime execution model for safety and performance. This is a work-in-progress project that currently supports a subset of Python features.\n\nProject goals:\n\n- **Safety**: Execute untrusted Python code safely without FFI or C dependencies, instead sandbox will call back to host to run foreign/external functions.\n- **Performance**: Fast execution through compile-time optimizations and efficient memory layout\n- **Simplicity**: Clean, understandable implementation focused on a Python subset\n- **Snapshotting and iteration**: Plan is to allow code to be iteratively executed and snapshotted at each function call\n- Targets the latest stable version of Python, currently Python 3.14\n\n## Important Security Notice\n\nIt's ABSOLUTELY CRITICAL that there's no way for code run in a Monty sandbox to access the host filesystem, or environment or to in any way \"escape the sandbox\".\n\n**Monty will be used to run untrusted, potentially malicious code.**\n\nMake sure there's no risk of this, either in the implementation, or in the public API that makes it more like that a developer using the pydantic_monty package might make such a mistake.\n\nPossible security risks to consider:\n* filesystem access\n* path traversal to access files the users did not intend to expose to the monty sandbox\n* memory errors - use of unsafe memory operations\n* excessive memory usage - evading monty's resource limits\n* infinite loops - evading monty's resource limits\n* network access - sockets, HTTP requests\n* subprocess/shell execution - os.system, subprocess, etc.\n* import system abuse - importing modules with side effects or accessing `__import__`\n* external function/callback misuse - callbacks run in host environment\n* deserialization attacks - loading untrusted serialized Monty/snapshot data\n* regex/string DoS - catastrophic backtracking or operations bypassing limits\n* information leakage via timing or error messages\n* Python/Javascript/Rust APIs that accidentally allow developers to expose their host to monty code\n\n## Bytecode VM Architecture\n\nMonty is implemented as a bytecode VM, same as CPython.\n\n### Reference Count Safety\n\nAll types that implement `DropWithHeap` hold heap references and **must** be cleaned up correctly on every code path — not just the happy path, but also early returns via `?`, `continue`, conditional branches, etc. A missed `drop_with_heap` on any branch leaks reference counts. There are three mechanisms for ensuring this, listed in order of preference:\n\n#### 1. `defer_drop!` macro (preferred)\n\nThe simplest and safest approach. Use `defer_drop!` (or `defer_drop_mut!` when mutable access to the value is needed) to bind a value into a guard that automatically drops it when scope exits — whether that's normal completion, early return via `?`, `continue`, or any other branch. The macro rebinds the value and heap variables as borrows from the guard, so you keep using them by name as before:\n\n```rust\nlet value = self.pop();\ndefer_drop!(value, heap);          // value is now &Value, heap is now &mut Heap\nlet result = value.py_repr(heap)?; // guard handles cleanup on all paths\n```\n\nBeyond safety, `defer_drop!` is often much more concise than inserting `drop_with_heap` calls in every branch of complex control flow.\n\n`defer_drop!` gives you an immutable reference to the value. Use `defer_drop_mut!` when you need a mutable reference (e.g. iterators, values you may swap):\n\n```rust\nlet iter = vm.heap.get_iter(iter_ref);\ndefer_drop_mut!(iter, vm);\nwhile let Some(item) = iter.for_next(vm)? { ... }\n```\n\n**Limitation:** because the macro rebinds the heap, it cannot be used inside `&mut self` methods where `self` owns the heap — first assign `let this = self;` and pass `this` instead.\n\n#### 2. `HeapGuard` (when you need control over the value's fate)\n\nUse `HeapGuard` directly when `defer_drop!` is too restrictive — specifically when you need to conditionally extract the value instead of dropping it. `HeapGuard` provides `into_inner()` and `into_parts()` to reclaim ownership, while its `Drop` impl still guarantees cleanup on all other paths:\n\n```rust\n// HeapGuard needed here because on success we push lhs back onto the stack\n// instead of dropping it\nlet mut lhs_guard = HeapGuard::new(self.pop(), self);\nlet (lhs, this) = lhs_guard.as_parts_mut();\n\nif lhs.py_iadd(rhs, this.heap)? {\n    let (lhs, this) = lhs_guard.into_parts(); // reclaim lhs, don't drop\n    this.push(lhs);\n    return Ok(());\n}\n// otherwise lhs_guard drops lhs automatically at scope exit\n```\n\n#### 3. Manual `drop_with_heap` (for trivially simple cases)\n\nFor very simple cases with a single linear code path and no branching between acquiring and releasing the value, a direct `drop_with_heap` call is fine:\n\n```rust\nlet iter = self.pop();\niter.drop_with_heap(&mut self.heap); // single path, no branching\n```\n\nAvoid manual `drop_with_heap` whenever there are multiple code paths (branching, `?`, `continue`, early returns) between acquiring and releasing the value — that is exactly where `defer_drop!` or `HeapGuard` prevent leaks by guaranteeing cleanup on every path.\n\n## Dev Commands\n\nDO NOT run `cargo build` or `cargo run`, it will fail because of issues with Python bindings.\n\nInstead use the following `make` commands:\n\n```bash\nmake install-py           Install python dependencies\nmake install-js           Install JS package dependencies\nmake install              Install the package, dependencies, and pre-commit for local development\nmake dev-py               Install the python package for development\nmake dev-js               Build the JS package (debug)\nmake lint-js              Lint JS code with oxlint\nmake test-js              Build and test the JS package\nmake dev-py-release       Install the python package for development with a release build\nmake dev-js-release       Build the JS package (release)\nmake dev-py-pgo           Install the python package for development with profile-guided optimization\nmake format-rs            Format Rust code with fmt\nmake format-py            Format Python code - WARNING be careful about this command as it may modify code and break tests silently!\nmake format-js            Format JS code with prettier\nmake format               Format Rust code, this does not format Python code as we have to be careful with that\nmake lint-rs              Lint Rust code with clippy and import checks\nmake clippy-fix           Fix Rust code with clippy\nmake lint-py              Lint Python code with ruff\nmake lint                 Lint the code with ruff and clippy\nmake format-lint-rs       Format and lint Rust code with fmt and clippy\nmake format-lint-py       Format and lint Python code with ruff\nmake test-no-features     Run rust tests without any features enabled\nmake test-ref-count-panic Run rust tests with ref-count-panic enabled\nmake test-ref-count-return Run rust tests with ref-count-return enabled\nmake test-cases           Run tests cases only\nmake test-type-checking   Run rust tests on monty_type_checking\nmake pytest               Run Python tests with pytest\nmake test-py              Build the python package (debug profile) and run tests\nmake test-docs            Test docs examples only\nmake test                 Run rust tests\nmake testcov              Run Rust tests with coverage, print table, and generate HTML report\nmake complete-tests       Fill in incomplete test expectations using CPython\nmake update-typeshed      Update vendored typeshed from upstream\nmake bench                Run benchmarks\nmake dev-bench            Run benchmarks to test with dev profile\nmake profile              Profile the code with pprof and generate flamegraphs\nmake type-sizes           Write type sizes for the crate to ./type-sizes.txt (requires nightly and top-type-sizes)\nmake main                 run linting and the most important tests\nmake help                 Show this help (usage: make help)\n```\n\nUse the /python-playground skill to check cpython and monty behavior.\n\n## Releasing\n\nSee [RELEASING.md](RELEASING.md) for the release process.\n\n## Exception\n\nIt's important that exceptions raised/returned by this library match those raised by Python.\n\nWherever you see an Exception with a repeated message, create a dedicated method to create that exception `src/exceptions.rs`.\n\nWhen writing exception messages, always check `src/exceptions.rs` for existing methods to generate that message.\n\n## Code style\n\nAvoid local imports, unless there's a very good reason, all imports should be at the top of the file.\n\nAvoid `fn my_func<T: MyTrait>(..., param: T)` style function definitions, STRONGLY prefer `fn my_func(param: impl MyTrait)` syntax since changes are more localized. This includes in trait definitions and implementations.\n\nAlso avoid using functions and structs via a path like `std::borrow::Cow::Owned(...)`, instead import `Cow` globally with `use std::borrow::Cow;`.\n\nNEVER use `allow()` in rust lint markers, instead use `expect()` so any unnecessary markers are removed. E.g. use\n\n```rs\n#[expect(clippy::too_many_arguments)]\n```\n\nNOT!\n\n```rs\n#[allow(clippy::too_many_arguments)]\n```\n\n### Docstrings and comments.\n\nIMPORTANT: every struct, enum and function should be a comprehensive but concise docstring to\nexplain what it does and why and any considerations or potential foot-guns of using that type.\n\nThe only exception is trait implementation methods where a docstring is not necessary if the method is self-explanatory.\n\nIt's important that docstrings cover the motivation and primary usage patterns of code, not just the simple \"what it does\".\n\nSimilarly, you should add comments to code, especially if the code is complex or esoteric.\n\nOnly add examples to docstrings of public functions and structs, examples should be <=8 lines, if the example is more, remove it.\n\nIf you add example code to docstrings, it must be run in tests. NEVER add examples that are ignored.\n\nIf you encounter a comment or docstring that's out of date - you MUST update it to be correct.\n\nSimilarly, if you encounter code that has no docstrings or comments, or they are minimal, you should add more detail.\n\nNOTE: COMMENTS AND DOCSTRINGS ARE EXTREMELY IMPORTANT TO THE LONG TERM HEALTH OF THE PROJECT.\n\n## Tests\n\nDo **NOT** write tests within modules unless explicitly prompted to do so.\n\nTests should live in the relevant `tests/` directory.\n\nCommands:\n\n```bash\n# Build the project\ncargo build\n\n# Run tests (this is the best way to run all tests as it enables the ref-count-panic feature)\nmake test-ref-count-panic\n\n# Run crates/monty/test_cases tests only\nmake test-cases\n\n# Run a specific test\ncargo test -p monty --test datatest_runner --features ref-count-panic str__ops\n\n# Run the interpreter on a Python file\ncargo run -- <file.py>\n```\n\nSee more test commands above.\n\n### Experimentation and Playground\n\nRead `Makefile` for other useful commands.\n\nDO NOT run `cargo run --`, it will fail because of issues with Python bindings.\n\nYou can use the `./playground` directory (excluded from git, create with `mkdir -p playground`) to write files\nwhen you want to experiment by running a file with cpython or monty, e.g.:\n* `python3 playground/test.py` to run the file with cpython\n* `cargo run -- playground/test.py` to run the file with monty\n\nDO NOT use `/tmp` or pipe code to the interpreter as it requires extra permissions and can slow you down!\n\nMore details in the \"python-playground\" skill.\n\n### Test File Structure\n\nMost functionality should be tested via python files in the `crates/monty/test_cases` directory.\n\n**DO NOT create many small test files.** This would be unmaintainable.\n\nALWAYS consolidate related tests into single files using multiple `assert` statements. Follow `crates/monty/test_cases/fstring__all.py` as the gold standard pattern:\n\n```python\n# === Section name ===\n# brief comment if needed\nassert condition, 'descriptive message'\nassert another_condition, 'another descriptive message'\n\n# === Next section ===\nx = setup_value\nassert x == expected, 'test description'\n```\n\nEach `assert` should have a descriptive message.\n\nDo NOT Write tests like `assert 'thing' in msg` it's lazy and inexact unless explicitly told to do so, instead write tests like `assert msg == 'expected message'` to ensure clarity and accuracy and most importantly, to identify differences between Monty and CPython.\n\n### When to Create Separate Test Files\n\nOnly create a separate test file when you MUST use one of these special expectation formats:\n\n- `\"\"\"TRACEBACK:...\"\"\"` - Test expects an exception with full traceback (PREFERRED for error tests)\n- `# Raise=Exception('message')` - Test expects an exception without traceback verification - NOT RECOMMENDED, use `TRACEBACK` instead\n- `# ref-counts={...}` - Test checks reference counts (special mode)\n- you're writing tests for a different behavior or section of the language\n\nFor everything else, **add asserts to an existing test file** or create ONE consolidated file for the feature.\n\n### File Naming\n\nName files by feature, not by micro-variant:\n- ✅ `str__ops.py` - all string operations (add, iadd, len, etc.)\n- ✅ `list__methods.py` - all list method tests\n- ❌ `str__add_basic.py`, `str__add_empty.py`, `str__add_multiple.py` - TOO GRANULAR\n\n### Expectation Formats (use sparingly)\n\nOnly use these when `assert` won't work (on last line of file):\n- `# Return=value` - Check `repr()` output (prefer assert instead)\n- `# Return.str=value` - Check `str()` output (prefer assert instead)\n- `# Return.type=typename` - Check `type()` output (prefer assert instead)\n- `# Raise=Exception('message')` - Expect exception without traceback (REQUIRES separate file)\n- `\"\"\"TRACEBACK:...\"\"\"` - Expect exception with full traceback (PREFERRED over `# Raise=`)\n- `# ref-counts={...}` - Check reference counts (REQUIRES separate file)\n- No expectation comment - Assert-based test (PREFERRED)\n\nDo NOT use `# Return=` when you could use `assert` instead\n\n### Traceback Tests (Preferred for Errors)\n\nFor tests that expect exceptions, **prefer traceback tests over `# Raise=`** because they verify:\n- The full traceback with all stack frames\n- Correct line numbers for each frame\n- Function names in the traceback\n- The caret markers (`~`) pointing to the error location\n\nTraceback test format - add a triple-quoted string at the end of the file starting with `\\nTRACEBACK:`:\n```python\ndef foo():\n    raise ValueError('oops')\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"my_test.py\", line 4, in <module>\n    foo()\n    ~~~~~\n  File \"my_test.py\", line 2, in foo\n    raise ValueError('oops')\nValueError: oops\n\"\"\"\n```\n\nKey points:\n- The filename in the traceback should match the test file name (just the basename, not the full path)\n- Use `~` for caret markers (the test runner normalizes CPython's `^` to `~`)\n- The `<module>` frame name is used for top-level code\n- Tests run against both Monty and CPython, so the traceback must match both\n\nOnly use `# Raise=` when you only care about the exception type/message and not the traceback.\n\n### Python fixture markers\n\nYou may mark python files with:\n* `# call-external` to support calling external functions\n* `# run-async` to support running async code\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nNever mark tests as:\n- `# xfail=cpython` - Test is required to fail on CPython\n- `# xfail=monty` - Test is required to fail on Monty\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nAll these markers must be at the start of comment lines to be recognized.\n\n### Other Notes\n\n- Prefer single quotes for strings in Python tests\n- Do NOT add `# noqa` or  `# pyright: ignore` comments to test code, instead add the failing code to `pyproject.toml`\n- The ONLY exception is `await` expressions outside of async functions, where you should add `# pyright: ignore`\n- Run `make lint-py` after adding tests\n- Use `make complete-tests` to fill in blank expectations\n- Tests run via `datatest-stable` harness in `tests/datatest_runner.rs`, use `make test-cases` to run them\n\n## Python Package (`pydantic-monty`)\n\nThe Python package provides Python bindings for the Monty interpreter, located in `crates/monty-python/`.\n\n### Structure\n\n- `crates/monty-python/src/` - Rust source for PyO3 bindings\n- `crates/monty-python/python/pydantic_monty/_monty.pyi` - Type stubs for the Python module\n- `crates/monty-python/tests/` - Python tests using pytest\n\n### Building and Testing\n\nDependencies needed for python testing are installed in `crates/monty-python/pyproject.toml`.\nTo install these dependencies, use `uv sync --all-packages --only-dev`.\n\n```bash\n# Build the Python package for development (required before running tests)\nmake dev-py\n\n# Run Python tests\nmake test-py\n\n# Or run pytest directly (after dev-py)\nuv run pytest\n\n# Run a specific test file\nuv run pytest crates/monty-python/tests/test_basic.py\n\n# Run a specific test\nuv run pytest crates/monty-python/tests/test_basic.py::test_simple_expression\n```\n\n### Python Test Guidelines\n\nCheck and follow the style of other python tests.\n\nMake sure you put tests in the correct file.\n\n**DO NOT use python/pytest tests for `monty` core functionality!** When testing core functionality, add tests to `crates/monty/test_cases/` or `crates/monty/tests/`. Only use python/pytest tests for `pydantic_monty` functionality testing.\n\n**NEVER use class-based tests.** All tests should be simple functions.\n\nUse `@pytest.mark.parametrize` whenever testing multiple similar cases.\n\nUse `snapshot` from `inline-snapshot` for all test asserts.\n\nNEVER do the lazy `assert '...' in ...` instead always do `assert value == snapshot()`,\nthen run the test and inline-snapshot will fill in the missing value in the `snapshot()` call.\n\nUse `pytest.raises` for expected exceptions, like this\n\n```py\nwith pytest.raises(ValueError) as exc_info:\n    m.run(print_callback=callback)\nassert exc_info.value.args[0] == snapshot('stopped at 3')\n```\n\n## Reference Counting\n\nHeap-allocated values (`Value::Ref`) use manual reference counting. Key rules:\n\n- **Cloning**: Use `clone_with_heap(heap)` which increments refcounts for `Ref` variants.\n- **Dropping**: Call `drop_with_heap(heap)` when discarding an `Value` that may be a `Ref`.\n\nContainer types (`List`, `Tuple`, `Dict`) also have `clone_with_heap()` methods.\n\n**Resource limits**: When resource limits (allocations, memory, time) are exceeded, execution terminates with a `ResourceError`. No guarantees are made about the state of the heap or reference counts after a resource limit is exceeded. The heap may contain orphaned objects with incorrect refcounts. This is acceptable because resource exhaustion is a terminal error - the execution context should be discarded.\n\n## NOTES\n\nALWAYS consider code quality when adding new code, if functions are getting too complex or code is duplicated, move relevant logic to a new file.\nMake sure functions are added in the most logical place, e.g. as methods on a struct where appropriate.\n\nThe code should follow the \"newspaper\" style where public and primary functions are at the top of the file, followed by private functions and utilities.\nALWAYS put utility, private functions and \"sub functions\" underneath the function they're used in.\n\nIt is important to the long term health of the project and maintainability of the codebase that code is well structured and organized, this is very important.\n\nALWAYS run `make format-rs` and `make lint-rs` after making changes to rust code and fix all suggestions to maintain code quality.\n\nALWAYS run `make lint-py` after making changes to python code and fix all suggestions to maintain code quality.\n\nALWAYS update this file when it is out of date.\n\nNEVER add imports anywhere except at the top of the file, this applies to both python and rust.\n\nNEVER write `unsafe` code, if you think you need to write unsafe code, explicitly ask the user or leave a `todo!()` with a suggestion and explanation.\n\n## JavaScript Package (`monty-js`)\n\nThe JavaScript package provides Node.js bindings for the Monty interpreter via napi-rs, located in `crates/monty-js/`.\n\n### Structure\n\n- `crates/monty-js/src/lib.rs` - Rust source for napi-rs bindings\n- `crates/monty-js/index.js` - Auto-generated JS loader that detects platform and loads the appropriate native binding\n- `crates/monty-js/index.d.ts` - TypeScript type declarations (auto-generated)\n- `crates/monty-js/__test__/` - Tests using ava\n\n### Current API\n\nThe package exposes:\n\n- `Monty` class - Parse and execute Python code with inputs, external functions, and resource limits\n- `MontySnapshot` / `MontyComplete` - For iterative execution with `start()` / `resume()`\n- `runMontyAsync()` - Helper for async external functions\n- `MontySyntaxError` / `MontyRuntimeError` / `MontyTypingError` - Error classes\n\n```ts\nimport { Monty, MontySnapshot, runMontyAsync } from '@pydantic/monty'\n\n// Basic execution\nconst m = new Monty('x + 1', { inputs: ['x'] })\nconst result = m.run({ inputs: { x: 10 } }) // returns 11\n\n// Iterative execution for external functions\nconst m2 = new Monty('fetch(url)', { inputs: ['url'], externalFunctions: ['fetch'] })\nlet progress = m2.start({ inputs: { url: 'https://...' } })\nif (progress instanceof MontySnapshot) {\n  progress = progress.resume({ returnValue: 'response data' })\n}\n```\n\nSee `crates/monty-js/README.md` for full API documentation.\n\n### Building and Testing\n\n```bash\n# Install dependencies\nmake install-js\n\n# Build native binding (debug)\nmake build-js\n\n# Build native binding (release)\nmake build-js-release\n\n# Run tests\nmake test-js\n\n# Format JavaScript code\nmake format-js\n\n# Lint JavaScript code\nmake lint-js\n```\n\nOr run directly in `crates/monty-js`:\n\n```bash\nnpm install\nnpm run build        # release build\nnpm run build:debug  # debug build\nnpm test\n```\n\n### JavaScript Test Guidelines\n\n- Tests use [ava](https://github.com/avajs/ava) and live in `crates/monty-js/__test__/`\n- Tests are written in TypeScript\n- Follow the existing test style in the `__test__/` directory\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\n    \"crates/monty\",\n    \"crates/monty-cli\",\n    \"crates/monty-python\",\n    \"crates/monty-js\",\n    \"crates/monty-type-checking\",\n    \"crates/monty-typeshed\",\n    \"crates/fuzz\"\n]\ndefault-members = [\"crates/monty-cli\"]\n\n[workspace.package]\nedition = \"2024\"\nversion = \"0.0.8\"\nrust-version = \"1.90\"\nlicense = \"MIT\"\nauthors = [\"Samuel Colvin <samuel@pydantic.dev>\"]\ndescription = \"A sandboxed, snapshotable Python interpreter written in Rust.\"\ncategories = [\"compilers\", \"emulators\", \"development-tools\"]\nkeywords = [\"python\", \"interpreter\", \"sandbox\", \"embedded\"]\nhomepage = \"https://github.com/pydantic/monty/\"\nrepository = \"https://github.com/pydantic/monty/\"\ndocumentation = \"https://github.com/pydantic/monty/\"\n\n[profile.release]\nlto = \"fat\"\ncodegen-units = 1\nstrip = true\n\n[profile.profiling]\ninherits = \"release\"\ndebug = true\nstrip = false\nlto = false\n\n[workspace.dependencies]\n# ruff, ty and related crates\nruff_python_parser = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ruff_python_parser\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\" }\nruff_python_ast = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ruff_python_ast\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\" }\nruff_text_size = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ruff_text_size\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\" }\nruff_db = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ruff_db\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\", features = [\"serde\"] }\nty_python_semantic = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ty_python_semantic\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\" }\nty_module_resolver = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ty_module_resolver\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\" }\nty_vendored = { git = \"https://github.com/astral-sh/ruff.git\", package = \"ty_vendored\", rev = \"6ded4bed1651e30b34dd04cdaa50c763036abb0d\" }\n# salsa version matches current main of ruff\nsalsa = { git = \"https://github.com/salsa-rs/salsa.git\", rev = \"53421c2fff87426fa0bb51cab06632b87646de13\", default-features = false, features = [\n    \"compact_str\",\n    \"macros\",\n    \"salsa_unstable\",\n    \"inventory\",\n] }\n# bigint and related crates\nnum-bigint = { version = \"0.4\", features = [\"serde\"] }\nnum-traits = \"0.2\"\nnum-integer = \"0.1\"\n# others\nindexmap = { version = \"2.9\", features = [\"serde\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\npostcard = { version = \"1.1\", features = [\"alloc\"] }\nsha2 = \"0.10\"\npretty_assertions = \"1.4\"\n\n[workspace.lints.rust]\n# codspeed cfg is set by codspeed-criterion-compat when running in CodSpeed environment\nunexpected_cfgs = { level = \"warn\", check-cfg = ['cfg(codspeed)'] }\n\n[workspace.lints.rustdoc]\ninvalid_codeblock_attributes = \"allow\"\n\n[workspace.lints.clippy]\ndbg_macro = \"warn\"\nuse_self = \"warn\"\nallow_attributes = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nredundant_clone = \"warn\"\n\n# in general we lint against the pedantic group, but we will whitelist\n# certain lints which we don't want to enforce\npedantic = { level = \"warn\", priority = -1 }\ncast_precision_loss = \"allow\"\ndoc_markdown = \"allow\"\nmatch_same_arms = \"allow\"\nmissing_errors_doc = \"allow\"\nsimilar_names = \"allow\"\ntoo_many_lines = \"allow\"\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) Pydantic Services Inc. 2026 to present\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".DEFAULT_GOAL := main\n\n.PHONY: .cargo\n.cargo: ## Check that cargo is installed\n\t@cargo --version || echo 'Please install cargo: https://github.com/rust-lang/cargo'\n\n.PHONY: .uv\n.uv: ## Check that uv is installed\n\t@uv --version || echo 'Please install uv: https://docs.astral.sh/uv/getting-started/installation/'\n\n.PHONY: .pre-commit\n.pre-commit: ## Check that pre-commit is installed\n\t@pre-commit -V || echo 'Please install pre-commit: https://pre-commit.com/'\n\n.PHONY: install-py\ninstall-py: .uv ## Install python dependencies\n\t# --only-dev to avoid building the python package, use make dev-py for that\n\tuv sync --all-packages --only-dev\n\n.PHONY: install-js\ninstall-js: ## Install JS package dependencies\n\tcd crates/monty-js && npm install\n\n.PHONY: install\ninstall: .cargo .pre-commit install-py install-js ## Install the package, dependencies, and pre-commit for local development\n\tcargo check --workspace\n\tpre-commit install --install-hooks\n\n.PHONY: dev-py\ndev-py: ## Install the python package for development\n\tuv run maturin develop --uv -m crates/monty-python/Cargo.toml\n\n.PHONY: dev-js\ndev-js: ## Build the JS package (debug)\n\tcd crates/monty-js && npm run build:debug\n\n.PHONY: lint-js\nlint-js: install-js ## Lint JS code with oxlint\n\tcd crates/monty-js && npm run lint\n\n.PHONY: test-js\ntest-js: dev-js ## Build and test the JS package\n\tcd crates/monty-js && npm test\n\n.PHONY: smoke-test-js\nsmoke-test-js: ## Run smoke test for JS package (builds, packs, and tests installation)\n\tcd crates/monty-js && npm run smoke-test\n\n.PHONY: dev-py-release\ndev-py-release: ## Install the python package for development with a release build\n\tuv run maturin develop --uv -m crates/monty-python/Cargo.toml --release\n\n.PHONY: dev-js-release\ndev-js-release: ## Build the JS package (release)\n\tcd crates/monty-js && npm run build\n\n.PHONY: dev-py-pgo\ndev-py-pgo: ## Install the python package for development with profile-guided optimization\n\t$(eval PROFDATA := $(shell mktemp -d))\n\tRUSTFLAGS='-Cprofile-generate=$(PROFDATA)' uv run maturin develop --uv -m crates/monty-python/Cargo.toml --release\n\tuv run --package pydantic-monty --only-dev pytest crates/monty-python/tests -k \"not test_parallel_exec\"\n\t$(eval LLVM_PROFDATA := $(shell rustup run stable bash -c 'echo $$RUSTUP_HOME/toolchains/$$RUSTUP_TOOLCHAIN/lib/rustlib/$$(rustc -Vv | grep host | cut -d \" \" -f 2)/bin/llvm-profdata'))\n\t$(LLVM_PROFDATA) merge -o $(PROFDATA)/merged.profdata $(PROFDATA)\n\tRUSTFLAGS='-Cprofile-use=$(PROFDATA)/merged.profdata' $(uv-run-no-sync) maturin develop --uv -m crates/monty-python/Cargo.toml --release\n\t@rm -rf $(PROFDATA)\n\n.PHONY: format-rs\nformat-rs:  ## Format Rust code with fmt\n\t@cargo +nightly fmt --version\n\tcargo +nightly fmt --all\n\n.PHONY: format-py\nformat-py: ## Format Python code - WARNING be careful about this command as it may modify code and break tests silently!\n\tuv run ruff format\n\tuv run ruff check --fix --fix-only\n\n.PHONY: format-js\nformat-js: install-js ## Format JS code with prettier\n\tcd crates/monty-js && npm run format:prettier\n\n.PHONY: format\nformat: format-rs format-py format-js ## Format Rust code, this does not format Python code as we have to be careful with that\n\n.PHONY: lint-rs\nlint-rs:  ## Lint Rust code with clippy and import checks\n\t@cargo clippy --version\n\tcargo clippy --workspace --tests --bench main -- -D warnings\n\tcargo clippy --workspace --tests --all-features -- -D warnings\n\tuv run scripts/check_imports.py\n\n.PHONY: clippy-fix\nclippy-fix: ## Fix Rust code with clippy\n\tcargo clippy --workspace --tests --bench main --all-features --fix --allow-dirty\n\n.PHONY: lint-py\nlint-py: dev-py ## Lint Python code with ruff\n\tuv run ruff format --check\n\tuv run ruff check\n\tuv run basedpyright\n\t# mypy-stubtest requires a build of the python package, hence dev-py\n\tuv run -m mypy.stubtest pydantic_monty._monty --ignore-disjoint-bases\n\n.PHONY: lint\nlint: lint-rs lint-py ## Lint the code with ruff and clippy\n\n.PHONY: format-lint-rs\nformat-lint-rs: format-rs lint-rs ## Format and lint Rust code with fmt and clippy\n\n.PHONY: format-lint-py\nformat-lint-py: format-py lint-py ## Format and lint Python code with ruff\n\n.PHONY: test-no-features\ntest-no-features: ## Run rust tests without any features enabled\n\tcargo test -p monty\n\n.PHONY: test-ref-count-panic\ntest-ref-count-panic: ## Run rust tests with ref-count-panic enabled\n\tcargo test -p monty --features ref-count-panic\n\n.PHONY: test-ref-count-return\ntest-ref-count-return: ## Run rust tests with ref-count-return enabled\n\tcargo test -p monty --features ref-count-return\n\n.PHONY: test-cases\ntest-cases: ## Run tests cases only\n\tcargo test -p monty --test datatest_runner\n\n.PHONY: test-type-checking\ntest-type-checking: ## Run rust tests on monty_type_checking\n\tcargo test -p monty_type_checking -p monty_typeshed\n\n.PHONY: pytest\npytest: ## Run Python tests with pytest\n\tuv run --package pydantic-monty --only-dev pytest crates/monty-python/tests\n\n.PHONY: test-py\ntest-py: dev-py pytest ## Build the python package (debug profile) and run tests\n\n.PHONY: test-docs\ntest-docs: dev-py ## Test docs examples only\n\tuv run --package pydantic-monty --only-dev pytest crates/monty-python/tests/test_readme_examples.py\n\tcargo test --doc -p monty\n\n.PHONY: test\ntest: test-ref-count-panic test-ref-count-return test-no-features test-type-checking test-py ## Run rust tests\n\n.PHONY: testcov\ntestcov: ## Run Rust tests with coverage, print table, and generate HTML report\n\t@cargo llvm-cov --version > /dev/null 2>&1 || echo 'Please run: `cargo install cargo-llvm-cov`'\n\tcargo llvm-cov clean --workspace\n\techo \"coverage for `make test-no-features`\"\n\tcargo llvm-cov --no-report -p monty\n\techo \"coverage for `make test-ref-count-panic`\"\n\tcargo llvm-cov --no-report -p monty --features ref-count-panic\n\techo \"coverage for `make test-ref-count-return`\"\n\tcargo llvm-cov --no-report -p monty --features ref-count-return\n\techo \"coverage for `make test-type-checking`\"\n\tcargo llvm-cov --no-report -p monty_type_checking -p monty_typeshed\n\techo \"Generating reports:\"\n\tcargo llvm-cov report --ignore-filename-regex '(tests/|test_cases/|/tests\\.rs$$)'\n\tcargo llvm-cov report --html --ignore-filename-regex '(tests/|test_cases/|/tests\\.rs$$)'\n\t@echo \"\"\n\t@echo \"HTML report: $${CARGO_TARGET_DIR:-target}/llvm-cov/html/index.html\"\n\n.PHONY: complete-tests\ncomplete-tests: ## Fill in incomplete test expectations using CPython\n\tuv run scripts/complete_tests.py\n\n.PHONY: update-typeshed\nupdate-typeshed: ## Update vendored typeshed from upstream\n\tuv run crates/monty-typeshed/update.py\n\tuv run ruff format\n\tuv run ruff check --fix --fix-only --silent\n\n.PHONY: bench\nbench: ## Run benchmarks\n\tcargo bench -p monty --bench main\n\n.PHONY: dev-bench\ndev-bench: ## Run benchmarks to test with dev profile\n\tcargo bench --profile dev -p monty --bench main -- --test\n\n.PHONY: profile\nprofile: ## Profile the code with pprof and generate flamegraphs\n\tcargo bench -p monty --bench main --profile profiling -- --profile-time=10\n\tuv run scripts/flamegraph_to_text.py\n\n.PHONY: type-sizes\ntype-sizes: ## Write type sizes for the crate to ./type-sizes.txt (requires nightly and top-type-sizes)\n\tRUSTFLAGS=\"-Zprint-type-sizes\" cargo +nightly build -j1 2>&1 | top-type-sizes -f '^monty.*' > type-sizes.txt\n\t@echo \"Type sizes written to ./type-sizes.txt\"\n\n.PHONY: fuzz-string_input_panic\nfuzz-string_input_panic: ## Run the `string_input_panic` fuzz target\n\tcargo +nightly fuzz run --fuzz-dir crates/fuzz string_input_panic\n\n.PHONY: fuzz-tokens_input_panic\nfuzz-tokens_input_panic: ## Run the `tokens_input_panic` fuzz target (structured token input)\n\tcargo +nightly fuzz run --fuzz-dir crates/fuzz tokens_input_panic\n\n.PHONY: main\nmain: lint test-ref-count-panic test-py ## run linting and the most important tests\n\n# (must stay last!)\n.PHONY: help\nhelp: ## Show this help (usage: make help)\n\t@echo \"Usage: make [recipe]\"\n\t@echo \"Recipes:\"\n\t@awk '/^[a-zA-Z0-9_-]+:.*?##/ { \\\n\t    helpMessage = match($$0, /## (.*)/); \\\n\t        if (helpMessage) { \\\n\t            recipe = $$1; \\\n\t            sub(/:/, \"\", recipe); \\\n\t            printf \"  \\033[36mmake %-20s\\033[0m %s\\n\", recipe, substr($$0, RSTART + 3, RLENGTH); \\\n\t    } \\\n\t}' $(MAKEFILE_LIST)\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <h1>Monty</h1>\n</div>\n<div align=\"center\">\n  <h3>A minimal, secure Python interpreter written in Rust for use by AI.</h3>\n</div>\n<div align=\"center\">\n  <a href=\"https://github.com/pydantic/monty/actions/workflows/ci.yml?query=branch%3Amain\"><img src=\"https://github.com/pydantic/monty/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"></a>\n  <a href=\"https://codspeed.io/pydantic/monty?utm_source=badge\"><img src=\"https://img.shields.io/badge/CodSpeed-Performance%20Tracked-blue?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOCAwTDAgOEw4IDE2TDE2IDhMOCAwWiIgZmlsbD0id2hpdGUiLz48L3N2Zz4=\" alt=\"Codspeed\"></a>\n  <a href=\"https://codecov.io/gh/pydantic/monty\"><img src=\"https://codecov.io/gh/pydantic/monty/graph/badge.svg?token=HX4RDQX5OG\" alt=\"Coverage\"></a>\n  <a href=\"https://pypi.python.org/pypi/pydantic-monty\"><img src=\"https://img.shields.io/pypi/v/pydantic-monty.svg\" alt=\"PyPI\"></a>\n  <a href=\"https://github.com/pydantic/monty\"><img src=\"https://img.shields.io/pypi/pyversions/pydantic-monty.svg\" alt=\"versions\"></a>\n  <a href=\"https://github.com/pydantic/monty/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/pydantic/monty.svg?v=2\" alt=\"license\"></a>\n  <a href=\"https://logfire.pydantic.dev/docs/join-slack/\"><img src=\"https://img.shields.io/badge/Slack-Join%20Slack-4A154B?logo=slack\" alt=\"Join Slack\" /></a>\n</div>\n\n---\n\n**Experimental** - This project is still in development, and not ready for the prime time.\n\nA minimal, secure Python interpreter written in Rust for use by AI.\n\nMonty avoids the cost, latency, complexity and general faff of using a full container based sandbox for running LLM generated code.\n\nInstead, it lets you safely run Python code written by an LLM embedded in your agent, with startup times measured in single digit microseconds not hundreds of milliseconds.\n\nWhat Monty **can** do:\n\n- Run a reasonable subset of Python code - enough for your agent to express what it wants to do\n- Completely block access to the host environment: filesystem, env variables and network access are all implemented via external function calls the developer can control\n- Call functions on the host - only functions you give it access to\n- Run typechecking - monty supports full modern python type hints and comes with [ty](https://docs.astral.sh/ty/) included in a single binary to run typechecking\n- Be snapshotted to bytes at external function calls, meaning you can store the interpreter state in a file or database, and resume later\n- Startup extremely fast (<1μs to go from code to execution result), and has runtime performance that is similar to CPython (generally between 5x faster and 5x slower)\n- Be called from Rust, Python, or Javascript - because Monty has no dependencies on cpython, you can use it anywhere you can run Rust\n- Control resource usage - Monty can track memory usage, allocations, stack depth, and execution time and cancel execution if it exceeds preset limits\n- Collect stdout and stderr and return it to the caller\n- Run async or sync code on the host via async or sync code on the host\n- Use a small subset of the standard library: `sys`, `os`, `typing`, `asyncio`, `re`, `datetime` (soon), `dataclasses` (soon), `json` (soon)\n\nWhat Monty **cannot** do:\n\n- Use the rest of the standard library\n- Use third party libraries (like Pydantic), support for external python library is not a goal\n- define classes (support should come soon)\n- use match statements (again, support should come soon)\n\n---\n\nIn short, Monty is extremely limited and designed for **one** use case:\n\n**To run code written by agents.**\n\nFor motivation on why you might want to do this, see:\n\n- [Codemode](https://blog.cloudflare.com/code-mode/) from Cloudflare\n- [Programmatic Tool Calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) from Anthropic\n- [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) from Anthropic\n- [Smol Agents](https://github.com/huggingface/smolagents) from Hugging Face\n\nIn very simple terms, the idea of all the above is that LLMs can work faster, cheaper and more reliably if they're asked to write Python (or Javascript) code, instead of relying on traditional tool calling. Monty makes that possible without the complexity of a sandbox or risk of running code directly on the host.\n\n**Note:** Monty will (soon) be used to implement `codemode` in [Pydantic AI](https://github.com/pydantic/pydantic-ai)\n\n## Usage\n\nMonty can be called from Python, JavaScript/TypeScript or Rust.\n\n### Python\n\nTo install:\n\n```bash\nuv add pydantic-monty\n```\n\n(Or `pip install pydantic-monty` for the boomers)\n\nUsage:\n\n```python\nfrom typing import Any\n\nimport pydantic_monty\n\ncode = \"\"\"\nasync def agent(prompt: str, messages: Messages):\n    while True:\n        print(f'messages so far: {messages}')\n        output = await call_llm(prompt, messages)\n        if isinstance(output, str):\n            return output\n        messages.extend(output)\n\nawait agent(prompt, [])\n\"\"\"\n\ntype_definitions = \"\"\"\nfrom typing import Any\n\nMessages = list[dict[str, Any]]\n\nasync def call_llm(prompt: str, messages: Messages) -> str | Messages:\n    raise NotImplementedError()\n\nprompt: str = ''\n\"\"\"\n\nm = pydantic_monty.Monty(\n    code,\n    inputs=['prompt'],\n    script_name='agent.py',\n    type_check=True,\n    type_check_stubs=type_definitions,\n)\n\n\nMessages = list[dict[str, Any]]\n\n\nasync def call_llm(prompt: str, messages: Messages) -> str | Messages:\n    if len(messages) < 2:\n        return [{'role': 'system', 'content': 'example response'}]\n    else:\n        return f'example output, message count {len(messages)}'\n\n\nasync def main():\n    output = await pydantic_monty.run_monty_async(\n        m,\n        inputs={'prompt': 'testing'},\n        external_functions={'call_llm': call_llm},\n    )\n    print(output)\n    #> example output, message count 2\n\n\nif __name__ == '__main__':\n    import asyncio\n\n    asyncio.run(main())\n```\n\n#### Iterative Execution with External Functions\n\nUse `start()` and `resume()` to handle external function calls iteratively,\ngiving you control over each call:\n\n```python\nimport pydantic_monty\n\ncode = \"\"\"\ndata = fetch(url)\nlen(data)\n\"\"\"\n\nm = pydantic_monty.Monty(code, inputs=['url'])\n\n# Start execution - pauses when fetch() is called\nresult = m.start(inputs={'url': 'https://example.com'})\n\nprint(type(result))\n#> <class 'pydantic_monty.FunctionSnapshot'>\nprint(result.function_name)  # fetch\n#> fetch\nprint(result.args)\n#> ('https://example.com',)\n\n# Perform the actual fetch, then resume with the result\nresult = result.resume(return_value='hello world')\n\nprint(type(result))\n#> <class 'pydantic_monty.MontyComplete'>\nprint(result.output)\n#> 11\n```\n\n#### Serialization\n\nBoth `Monty` and snapshot types like `FunctionSnapshot` can be serialized to bytes and restored later.\nThis allows caching parsed code or suspending execution across process boundaries:\n\n```python\nimport pydantic_monty\n\n# Serialize parsed code to avoid re-parsing\nm = pydantic_monty.Monty('x + 1', inputs=['x'])\ndata = m.dump()\n\n# Later, restore and run\nm2 = pydantic_monty.Monty.load(data)\nprint(m2.run(inputs={'x': 41}))\n#> 42\n\n# Serialize execution state mid-flight\nm = pydantic_monty.Monty('fetch(url)', inputs=['url'])\nprogress = m.start(inputs={'url': 'https://example.com'})\nstate = progress.dump()\n\n# Later, restore and resume (e.g., in a different process)\nprogress2 = pydantic_monty.load_snapshot(state)\nresult = progress2.resume(return_value='response data')\nprint(result.output)\n#> response data\n```\n\n### Rust\n\n```rust\nuse monty::{MontyRun, MontyObject, NoLimitTracker, PrintWriter};\n\nlet code = r#\"\ndef fib(n):\n    if n <= 1:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nfib(x)\n\"#;\n\nlet runner = MontyRun::new(code.to_owned(), \"fib.py\", vec![\"x\".to_owned()]).unwrap();\nlet result = runner.run(vec![MontyObject::Int(10)], NoLimitTracker, PrintWriter::Stdout).unwrap();\nassert_eq!(result, MontyObject::Int(55));\n```\n\n#### Serialization\n\n`MontyRun` and `RunProgress` can be serialized using the `dump()` and `load()` methods:\n\n```rust\nuse monty::{MontyRun, MontyObject, NoLimitTracker, PrintWriter};\n\n// Serialize parsed code\nlet runner = MontyRun::new(\"x + 1\".to_owned(), \"main.py\", vec![\"x\".to_owned()]).unwrap();\nlet bytes = runner.dump().unwrap();\n\n// Later, restore and run\nlet runner2 = MontyRun::load(&bytes).unwrap();\nlet result = runner2.run(vec![MontyObject::Int(41)], NoLimitTracker, PrintWriter::Stdout).unwrap();\nassert_eq!(result, MontyObject::Int(42));\n```\n\n## PydanticAI Integration\n\nMonty will power code-mode in\n[Pydantic AI](https://github.com/pydantic/pydantic-ai). Instead of making\nsequential tool calls, the LLM writes Python code that calls your tools\nas functions and Monty executes it safely.\n\n```python test=\"skip\"\nimport asyncio\nimport json\n\nimport logfire\nfrom httpx import AsyncClient\nfrom pydantic_ai import Agent, RunContext\nfrom pydantic_ai.toolsets.code_mode import CodeModeToolset\nfrom pydantic_ai.toolsets.function import FunctionToolset\nfrom typing_extensions import TypedDict\n\nlogfire.configure()\nlogfire.instrument_pydantic_ai()\n\n\nclass LatLng(TypedDict):\n    lat: float\n    lng: float\n\n\nweather_toolset: FunctionToolset[AsyncClient] = FunctionToolset()\n\n\n@weather_toolset.tool\nasync def get_lat_lng(\n    ctx: RunContext[AsyncClient], location_description: str\n) -> LatLng:\n    \"\"\"Get the latitude and longitude of a location.\"\"\"\n    # NOTE: the response here will be random, and is not related to the location description.\n    r = await ctx.deps.get(\n        'https://demo-endpoints.pydantic.workers.dev/latlng',\n        params={'location': location_description},\n    )\n    r.raise_for_status()\n    return json.loads(r.content)\n\n\n@weather_toolset.tool\nasync def get_temp(ctx: RunContext[AsyncClient], lat: float, lng: float) -> float:\n    \"\"\"Get the temp at a location.\"\"\"\n    # NOTE: the responses here will be random, and are not related to the lat and lng.\n    r = await ctx.deps.get(\n        'https://demo-endpoints.pydantic.workers.dev/number',\n        params={'min': 10, 'max': 30},\n    )\n    r.raise_for_status()\n    return float(r.text)\n\n\n@weather_toolset.tool\nasync def get_weather_description(\n    ctx: RunContext[AsyncClient], lat: float, lng: float\n) -> str:\n    \"\"\"Get the weather description at a location.\"\"\"\n    # NOTE: the responses here will be random, and are not related to the lat and lng.\n    r = await ctx.deps.get(\n        'https://demo-endpoints.pydantic.workers.dev/weather',\n        params={'lat': lat, 'lng': lng},\n    )\n    r.raise_for_status()\n    return r.text\n\n\nagent = Agent(\n    'gateway/anthropic:claude-sonnet-4-5',\n    # toolsets=[weather_toolset],\n    toolsets=[CodeModeToolset(weather_toolset)],\n    deps_type=AsyncClient,\n)\n\n\nasync def main():\n    async with AsyncClient() as client:\n        await agent.run('Compare the weather of London, Paris, and Tokyo.', deps=client)\n\n\nif __name__ == '__main__':\n    asyncio.run(main())\n```\n\n# Alternatives\n\nThere are generally two responses when you show people Monty:\n\n1. Oh my god, this solves so many problems, I want it.\n2. Why not X?\n\nWhere X is some alternative technology. Oddly often these responses are combined, suggesting people have not yet found an alternative that works for them, but are incredulous that there's really no good alternative to creating an entire Python implementation from scratch.\n\nI'll try to run through the most obvious alternatives, and why there aren't right for what we wanted.\n\nNOTE: all these technologies are impressive and have widespread uses, this commentary on their limitations for our use case should not be seen as a criticism. Most of these solutions were not conceived with the goal of providing an LLM sandbox, which is why they're not necessary great at it.\n\n| Tech               | Language completeness | Security     | Start latency | FOSS       | Setup complexity | File mounting  | Snapshotting |\n| ------------------ | --------------------- | ------------ | ------------- | ---------- | ---------------- | -------------- | ------------ |\n| Monty              | partial               | strict       | 0.06ms        | free / OSS | easy             | easy           | easy         |\n| Docker             | full                  | good         | 195ms         | free / OSS | intermediate     | easy           | intermediate |\n| Pyodide            | full                  | poor         | 2800ms        | free / OSS | intermediate     | easy           | hard         |\n| starlark-rust      | very limited          | good         | 1.7ms         | free / OSS | easy             | not available? | impossible?  |\n| WASI / Wasmer      | partial, almost full  | strict       | 66ms          | free \\*    | intermediate     | easy           | intermediate |\n| sandboxing service | full                  | strict       | 1033ms        | not free   | intermediate     | hard           | intermediate |\n| YOLO Python        | full                  | non-existent | 0.1ms / 30ms  | free / OSS | easy             | easy / scary   | hard         |\n\nSee [./scripts/startup_performance.py](scripts/startup_performance.py) for the script used to calculate the startup performance numbers.\n\nDetails on each row below:\n\n### Monty\n\n- **Language completeness**: No classes (yet), limited stdlib, no third-party libraries\n- **Security**: Explicitly controlled filesystem, network, and env access, strict limits on execution time and memory usage\n- **Start latency**: Starts in microseconds\n- **Setup complexity**: just `pip install pydantic-monty` or `npm install @pydantic/monty`, ~4.5MB download\n- **File mounting**: Strictly controlled, see [#85](https://github.com/pydantic/monty/pull/85)\n- **Snapshotting**: Monty's pause and resume functionality with `dump()` and `load()` makes it trivial to pause, resume and fork execution\n\n### Docker\n\n- **Language completeness**: Full CPython with any library\n- **Security**: Process and filesystem isolation, network policies, but container escapes exist, memory limitation is possible\n- **Start latency**: Container startup overhead (~195ms measured)\n- **Setup complexity**: Requires Docker daemon, container images, orchestration, `python:3.14-alpine` is 50MB - docker can't be installed from PyPI\n- **File mounting**: Volume mounts work well\n- **Snapshotting**: Possible with durable execution solutions like Temporal, or snapshotting an image and saving it as a Docker image.\n\n### Pyodide\n\n- **Language completeness**: Full CPython compiled to WASM, almost all libraries available\n- **Security**: Relies on browser/WASM sandbox - not designed for server-side isolation, python code can run arbitrary code in the JS runtime, only deno allows isolation, memory limits are hard/impossible to enforce with deno\n- **Start latency**: WASM runtime loading is slow (~2800ms cold start)\n- **Setup complexity**: Need to load WASM runtime, handle async initialization, pyodide NPM package is ~12MB, deno is ~50MB - Pyodide can't be called with just PyPI packages\n- **File mounting**: Virtual filesystem via browser APIs\n- **Snapshotting**: Possible with durable execution solutions like Temporal presumably, but hard\n\n### starlark-rust\n\nSee [starlark-rust](https://github.com/facebook/starlark-rust).\n\n- **Language completeness**: Configuration language, not Python - no classes, exceptions, async\n- **Security**: Deterministic and hermetic by design\n- **Start latency**: runs embedded in the process like Monty, hence impressive startup time\n- **Setup complexity**: Usable in python via [starlark-pyo3](https://github.com/inducer/starlark-pyo3)\n- **File mounting**: No file handling by design AFAIK?\n- **Snapshotting**: Impossible AFAIK?\n\n### WASI / Wasmer\n\nRunning Python in WebAssembly via [Wasmer](https://wasmer.io/).\n\n- **Language completeness**: Full CPython, pure Python external packages work via mounting, external packages with C bindings don't work\n- **Security**: In principle WebAssembly should provide strong sandboxing guarantees.\n- **Start latency**: The [wasmer](https://pypi.org/project/wasmer/) python package hasn't been updated for 3 years and I couldn't find docs on calling Python in wasmer from Python, so I called it via subprocess. Start latency was 66ms.\n- **Setup complexity**: wasmer download is 100mb, the \"python/python\" package is 50mb.\n- **FOSS**: I marked this as \"free \\*\" since the cost is zero but not everything seems to be open source. As of 2026-02-10 the [`python/python` wasmer package](https://wasmer.io/python/python) package has no readme, no license, no source link and no indication of how it's built, the recently uploaded versions show size as \"0B\" although the download is ~50MB - the build process for the Python binary is not clear and transparent. _(If I'm wrong here, please create an issue to correct correct me)_\n- **File mounting**: Supported\n- **Snapshotting**: Supported via journaling\n\n### sandboxing service\n\nServices like [Daytona](https://daytona.io), [E2B](https://e2b.dev), [Modal](https://modal.com).\n\nThere are similar challenges, more setup complexity but lower network latency for setting up your own sandbox setup with k8s.\n\n- **Language completeness**: Full CPython with any library\n- **Security**: Professionally managed container isolation\n- **Start latency**: Network round-trip and container startup time. I got ~1s cold start time with Daytona EU from London, Daytona advertise sub 90ms latency, presumably that's for an existing container, not clear if it includes network latency\n- **FOSS**: Pay per execution or compute time, some implementations are open source\n- **Setup complexity**: API integration, auth tokens - fine for startups but generally a non-start for enterprises\n- **File mounting**: Upload/download via API calls\n- **Snapshotting**: Possible with durable execution solutions like Temporal, also the services offer some solutions for this, I think based con docker containers\n\n### YOLO Python\n\nRunning Python directly via `exec()` (~0.1ms) or subprocess (~30ms).\n\n- **Language completeness**: Full CPython with any library\n- **Security**: None - full filesystem, network, env vars, system commands\n- **Start latency**: Near-zero for `exec()`, ~30ms for subprocess\n- **Setup complexity**: None\n- **File mounting**: Direct filesystem access (that's the problem)\n- **Snapshotting**: Possible with durable execution solutions like Temporal\n"
  },
  {
    "path": "RELEASING.md",
    "content": "# Release Process\n\n## 1. Bump Version\n\nUpdate version in both files:\n\n```bash\n# Edit Cargo.toml - update workspace.package.version\n# Edit crates/monty-js/package.json - update version\n\n# Update Cargo.lock\nmake lint-rs\n```\n\nBoth `Cargo.toml` and `package.json` should have the same version (e.g., `0.0.2`).\n\n## 2. Commit and Push\n\n```bash\ngit add Cargo.toml Cargo.lock crates/monty-js/package.json\ngit commit -m \"Bump version to X.Y.Z\"\ngit push\n```\n\n## 3. Create Release via GitHub UI\n\n1. Go to https://github.com/pydantic/monty/releases/new\n2. Click \"Choose a tag\" and type the new tag name (e.g., `v0.0.2`)\n3. Select \"Create new tag on publish\"\n4. Set the release title (e.g., `v0.0.2`)\n5. Add release notes\n6. Click \"Publish release\"\n\n## 4. CI Handles Publishing\n\nOnce the tag is pushed, CI will:\n- Build wheels for all platforms\n- Publish to PyPI (`pydantic-monty`)\n- Publish to NPM (`@pydantic/monty`)\n\nMonitor the workflow at https://github.com/pydantic/monty/actions\n\n## Pre-release Tags\n\nFor pre-releases (alpha, beta, rc), use a tag like `v0.0.2-beta.1`:\n- PyPI: Published normally\n- NPM: Published with `--tag next` (not `latest`)\n"
  },
  {
    "path": "crates/fuzz/Cargo.toml",
    "content": "[package]\nname = \"monty-fuzz\"\npublish = false\nversion = { workspace = true }\nedition = { workspace = true }\n\n[package.metadata]\ncargo-fuzz = true\n\n[dependencies]\narbitrary = { version = \"1\", features = [\"derive\"] }\nlibfuzzer-sys = \"0.4\"\nmonty = { path = \"../monty\" }\n\n[[bin]]\nname = \"string_input_panic\"\npath = \"fuzz_targets/string_input_panic.rs\"\ntest = false\ndoc = false\nbench = false\n\n[[bin]]\nname = \"tokens_input_panic\"\npath = \"fuzz_targets/tokens_input_panic.rs\"\ntest = false\ndoc = false\nbench = false\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/fuzz/fuzz_targets/string_input_panic.rs",
    "content": "//! Fuzz target for testing that arbitrary Python code doesn't cause panics or crashes.\n//!\n//! This target feeds arbitrary byte sequences to the Monty interpreter and verifies that\n//! neither parsing nor execution causes the interpreter to panic or crash. Errors (parse\n//! errors, runtime errors, etc.) are expected and ignored - we only care about panics.\n//!\n//! Resource limits are enforced to prevent infinite loops and memory exhaustion.\n#![no_main]\n\nuse std::time::Duration;\n\nuse libfuzzer_sys::fuzz_target;\nuse monty::{LimitedTracker, MontyRun, PrintWriter, ResourceLimits};\n\n/// Resource limits for fuzzing - restrictive to prevent hangs and memory issues.\nfn fuzz_limits() -> LimitedTracker {\n    LimitedTracker::new(\n        ResourceLimits::new()\n            .max_allocations(10_000)\n            .max_memory(1024 * 1024) // 1 MB\n            .max_duration(Duration::from_millis(100)),\n    )\n}\n\nfuzz_target!(|code: String| {\n    // Try to parse the code\n    let Ok(runner) = MontyRun::new(\n        code.to_owned(),\n        \"fuzz.py\",\n        vec![], // no inputs\n    ) else {\n        return; // Parse errors are expected for random input\n    };\n\n    // Try to execute with resource limits - ignore all errors, we only care about panics/crashes\n    let _ = runner.run(vec![], fuzz_limits(), PrintWriter::Disabled);\n});\n"
  },
  {
    "path": "crates/fuzz/fuzz_targets/tokens_input_panic.rs",
    "content": "//! Fuzz target using structured token input instead of random strings.\n//!\n//! This generates more syntactically plausible Python code by combining\n//! tokens that represent common Python constructs. The fuzzer explores\n//! combinations of these tokens to find edge cases.\n#![no_main]\n\nuse std::{\n    fmt::{self, Display},\n    time::Duration,\n};\n\nuse arbitrary::Arbitrary;\nuse libfuzzer_sys::fuzz_target;\nuse monty::{LimitedTracker, MontyRun, PrintWriter, ResourceLimits};\n\n/// A token representing a piece of Python syntax.\n#[derive(Debug, Clone, Arbitrary)]\nenum Token {\n    // === Literals ===\n    String(StringLit),\n    Int(i64),\n    Float(FloatLit),\n    Bool(bool),\n    None,\n\n    // === Identifiers ===\n    Var(VarName),\n    Attr(AttrName),\n\n    // === Operators ===\n    BinOp(BinOp),\n    UnaryOp(UnaryOp),\n    CompareOp(CompareOp),\n    AugAssign(AugAssign),\n\n    // === Keywords ===\n    Keyword(Keyword),\n\n    // === Punctuation ===\n    LParen,\n    RParen,\n    LBracket,\n    RBracket,\n    LBrace,\n    RBrace,\n    Comma,\n    Colon,\n    Semicolon,\n    Dot,\n    Arrow,\n    Assign,\n    Walrus,\n\n    // === Whitespace/Structure ===\n    Space,\n    Newline,\n    Indent(IndentLevel),\n    Comment,\n}\n\n/// String literal variants.\n#[derive(Debug, Clone, Arbitrary)]\nenum StringLit {\n    Empty,\n    Short(ShortString),\n    FString(ShortString),\n    Raw(ShortString),\n    Bytes(ShortString),\n}\n\n/// Short string content (limited to avoid huge inputs).\n#[derive(Debug, Clone, Arbitrary)]\nenum ShortString {\n    Hello,\n    World,\n    Test,\n    Foo,\n    Bar,\n    Empty,\n    Space,\n    Newline,\n    Number,\n    Special,\n}\n\n/// Float literal (avoiding infinity/NaN issues).\n#[derive(Debug, Clone, Arbitrary)]\nenum FloatLit {\n    Zero,\n    One,\n    Half,\n    Pi,\n    Negative,\n    Small,\n    Large,\n}\n\n/// Common variable names.\n#[derive(Debug, Clone, Arbitrary)]\nenum VarName {\n    X,\n    Y,\n    Z,\n    A,\n    B,\n    C,\n    I,\n    J,\n    N,\n    Foo,\n    Bar,\n    Baz,\n    Spam,\n    Eggs,\n    Result,\n    Value,\n    Item,\n    Data,\n    Args,\n    Kwargs,\n    Self_,\n    Cls,\n}\n\n/// Common attribute names.\n#[derive(Debug, Clone, Arbitrary)]\nenum AttrName {\n    Append,\n    Pop,\n    Get,\n    Set,\n    Keys,\n    Values,\n    Items,\n    Join,\n    Split,\n    Strip,\n    Lower,\n    Upper,\n    Format,\n    Replace,\n    Find,\n    Count,\n    Sort,\n    Reverse,\n    Copy,\n    Clear,\n    Update,\n    Add,\n    Remove,\n}\n\n/// Binary operators.\n#[derive(Debug, Clone, Arbitrary)]\nenum BinOp {\n    Add,\n    Sub,\n    Mul,\n    Div,\n    FloorDiv,\n    Mod,\n    Pow,\n    MatMul,\n    BitAnd,\n    BitOr,\n    BitXor,\n    LShift,\n    RShift,\n    And,\n    Or,\n}\n\n/// Unary operators.\n#[derive(Debug, Clone, Arbitrary)]\nenum UnaryOp {\n    Neg,\n    Pos,\n    Not,\n    Invert,\n}\n\n/// Comparison operators.\n#[derive(Debug, Clone, Arbitrary)]\nenum CompareOp {\n    Eq,\n    Ne,\n    Lt,\n    Le,\n    Gt,\n    Ge,\n    Is,\n    IsNot,\n    In,\n    NotIn,\n}\n\n/// Augmented assignment operators.\n#[derive(Debug, Clone, Arbitrary)]\nenum AugAssign {\n    AddEq,\n    SubEq,\n    MulEq,\n    DivEq,\n    FloorDivEq,\n    ModEq,\n    PowEq,\n    AndEq,\n    OrEq,\n    XorEq,\n    LShiftEq,\n    RShiftEq,\n}\n\n/// Python keywords.\n#[derive(Debug, Clone, Arbitrary)]\nenum Keyword {\n    If,\n    Elif,\n    Else,\n    For,\n    While,\n    Break,\n    Continue,\n    Pass,\n    Return,\n    Def,\n    Lambda,\n    Async,\n    Await,\n    Try,\n    Except,\n    Finally,\n    Raise,\n    Assert,\n    Import,\n    From,\n    As,\n    Global,\n    Nonlocal,\n}\n\n/// Indentation levels (0-4 levels deep).\n#[derive(Debug, Clone, Arbitrary)]\nenum IndentLevel {\n    L0,\n    L1,\n    L2,\n    L3,\n    L4,\n}\n\nimpl Display for Token {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::String(s) => write!(f, \"{s}\"),\n            Self::Int(n) => write!(f, \"{n}\"),\n            Self::Float(fl) => write!(f, \"{fl}\"),\n            Self::Bool(true) => write!(f, \"True\"),\n            Self::Bool(false) => write!(f, \"False\"),\n            Self::None => write!(f, \"None\"),\n            Self::Var(v) => write!(f, \"{v}\"),\n            Self::Attr(a) => write!(f, \"{a}\"),\n            Self::BinOp(op) => write!(f, \"{op}\"),\n            Self::UnaryOp(op) => write!(f, \"{op}\"),\n            Self::CompareOp(op) => write!(f, \"{op}\"),\n            Self::AugAssign(op) => write!(f, \"{op}\"),\n            Self::Keyword(kw) => write!(f, \"{kw}\"),\n            Self::LParen => write!(f, \"(\"),\n            Self::RParen => write!(f, \")\"),\n            Self::LBracket => write!(f, \"[\"),\n            Self::RBracket => write!(f, \"]\"),\n            Self::LBrace => write!(f, \"{{\"),\n            Self::RBrace => write!(f, \"}}\"),\n            Self::Comma => write!(f, \",\"),\n            Self::Colon => write!(f, \":\"),\n            Self::Semicolon => write!(f, \";\"),\n            Self::Dot => write!(f, \".\"),\n            Self::Arrow => write!(f, \"->\"),\n            Self::Assign => write!(f, \"=\"),\n            Self::Walrus => write!(f, \":=\"),\n            Self::Space => write!(f, \" \"),\n            Self::Newline => writeln!(f),\n            Self::Indent(level) => write!(f, \"{level}\"),\n            Self::Comment => write!(f, \"# comment\"),\n        }\n    }\n}\n\nimpl Display for StringLit {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Empty => write!(f, \"''\"),\n            Self::Short(s) => write!(f, \"'{s}'\"),\n            Self::FString(s) => write!(f, \"f'{s}'\"),\n            Self::Raw(s) => write!(f, \"r'{s}'\"),\n            Self::Bytes(s) => write!(f, \"b'{s}'\"),\n        }\n    }\n}\n\nimpl Display for ShortString {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Hello => write!(f, \"hello\"),\n            Self::World => write!(f, \"world\"),\n            Self::Test => write!(f, \"test\"),\n            Self::Foo => write!(f, \"foo\"),\n            Self::Bar => write!(f, \"bar\"),\n            Self::Empty => write!(f, \"\"),\n            Self::Space => write!(f, \" \"),\n            Self::Newline => write!(f, \"\\\\n\"),\n            Self::Number => write!(f, \"123\"),\n            Self::Special => write!(f, \"{{}}\"),\n        }\n    }\n}\n\nimpl Display for FloatLit {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Zero => write!(f, \"0.0\"),\n            Self::One => write!(f, \"1.0\"),\n            Self::Half => write!(f, \"0.5\"),\n            Self::Pi => write!(f, \"3.14159\"),\n            Self::Negative => write!(f, \"-1.5\"),\n            Self::Small => write!(f, \"0.001\"),\n            Self::Large => write!(f, \"1e10\"),\n        }\n    }\n}\n\nimpl Display for VarName {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::X => write!(f, \"x\"),\n            Self::Y => write!(f, \"y\"),\n            Self::Z => write!(f, \"z\"),\n            Self::A => write!(f, \"a\"),\n            Self::B => write!(f, \"b\"),\n            Self::C => write!(f, \"c\"),\n            Self::I => write!(f, \"i\"),\n            Self::J => write!(f, \"j\"),\n            Self::N => write!(f, \"n\"),\n            Self::Foo => write!(f, \"foo\"),\n            Self::Bar => write!(f, \"bar\"),\n            Self::Baz => write!(f, \"baz\"),\n            Self::Spam => write!(f, \"spam\"),\n            Self::Eggs => write!(f, \"eggs\"),\n            Self::Result => write!(f, \"result\"),\n            Self::Value => write!(f, \"value\"),\n            Self::Item => write!(f, \"item\"),\n            Self::Data => write!(f, \"data\"),\n            Self::Args => write!(f, \"args\"),\n            Self::Kwargs => write!(f, \"kwargs\"),\n            Self::Self_ => write!(f, \"self\"),\n            Self::Cls => write!(f, \"cls\"),\n        }\n    }\n}\n\nimpl Display for AttrName {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Append => write!(f, \"append\"),\n            Self::Pop => write!(f, \"pop\"),\n            Self::Get => write!(f, \"get\"),\n            Self::Set => write!(f, \"set\"),\n            Self::Keys => write!(f, \"keys\"),\n            Self::Values => write!(f, \"values\"),\n            Self::Items => write!(f, \"items\"),\n            Self::Join => write!(f, \"join\"),\n            Self::Split => write!(f, \"split\"),\n            Self::Strip => write!(f, \"strip\"),\n            Self::Lower => write!(f, \"lower\"),\n            Self::Upper => write!(f, \"upper\"),\n            Self::Format => write!(f, \"format\"),\n            Self::Replace => write!(f, \"replace\"),\n            Self::Find => write!(f, \"find\"),\n            Self::Count => write!(f, \"count\"),\n            Self::Sort => write!(f, \"sort\"),\n            Self::Reverse => write!(f, \"reverse\"),\n            Self::Copy => write!(f, \"copy\"),\n            Self::Clear => write!(f, \"clear\"),\n            Self::Update => write!(f, \"update\"),\n            Self::Add => write!(f, \"add\"),\n            Self::Remove => write!(f, \"remove\"),\n        }\n    }\n}\n\nimpl Display for BinOp {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Add => write!(f, \" + \"),\n            Self::Sub => write!(f, \" - \"),\n            Self::Mul => write!(f, \" * \"),\n            Self::Div => write!(f, \" / \"),\n            Self::FloorDiv => write!(f, \" // \"),\n            Self::Mod => write!(f, \" % \"),\n            Self::Pow => write!(f, \" ** \"),\n            Self::MatMul => write!(f, \" @ \"),\n            Self::BitAnd => write!(f, \" & \"),\n            Self::BitOr => write!(f, \" | \"),\n            Self::BitXor => write!(f, \" ^ \"),\n            Self::LShift => write!(f, \" << \"),\n            Self::RShift => write!(f, \" >> \"),\n            Self::And => write!(f, \" and \"),\n            Self::Or => write!(f, \" or \"),\n        }\n    }\n}\n\nimpl Display for UnaryOp {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Neg => write!(f, \"-\"),\n            Self::Pos => write!(f, \"+\"),\n            Self::Not => write!(f, \"not \"),\n            Self::Invert => write!(f, \"~\"),\n        }\n    }\n}\n\nimpl Display for CompareOp {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Eq => write!(f, \" == \"),\n            Self::Ne => write!(f, \" != \"),\n            Self::Lt => write!(f, \" < \"),\n            Self::Le => write!(f, \" <= \"),\n            Self::Gt => write!(f, \" > \"),\n            Self::Ge => write!(f, \" >= \"),\n            Self::Is => write!(f, \" is \"),\n            Self::IsNot => write!(f, \" is not \"),\n            Self::In => write!(f, \" in \"),\n            Self::NotIn => write!(f, \" not in \"),\n        }\n    }\n}\n\nimpl Display for AugAssign {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::AddEq => write!(f, \" += \"),\n            Self::SubEq => write!(f, \" -= \"),\n            Self::MulEq => write!(f, \" *= \"),\n            Self::DivEq => write!(f, \" /= \"),\n            Self::FloorDivEq => write!(f, \" //= \"),\n            Self::ModEq => write!(f, \" %= \"),\n            Self::PowEq => write!(f, \" **= \"),\n            Self::AndEq => write!(f, \" &= \"),\n            Self::OrEq => write!(f, \" |= \"),\n            Self::XorEq => write!(f, \" ^= \"),\n            Self::LShiftEq => write!(f, \" <<= \"),\n            Self::RShiftEq => write!(f, \" >>= \"),\n        }\n    }\n}\n\nimpl Display for Keyword {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::If => write!(f, \"if \"),\n            Self::Elif => write!(f, \"elif \"),\n            Self::Else => write!(f, \"else\"),\n            Self::For => write!(f, \"for \"),\n            Self::While => write!(f, \"while \"),\n            Self::Break => write!(f, \"break\"),\n            Self::Continue => write!(f, \"continue\"),\n            Self::Pass => write!(f, \"pass\"),\n            Self::Return => write!(f, \"return \"),\n            Self::Def => write!(f, \"def \"),\n            Self::Lambda => write!(f, \"lambda \"),\n            Self::Async => write!(f, \"async \"),\n            Self::Await => write!(f, \"await \"),\n            Self::Try => write!(f, \"try\"),\n            Self::Except => write!(f, \"except \"),\n            Self::Finally => write!(f, \"finally\"),\n            Self::Raise => write!(f, \"raise \"),\n            Self::Assert => write!(f, \"assert \"),\n            Self::Import => write!(f, \"import \"),\n            Self::From => write!(f, \"from \"),\n            Self::As => write!(f, \" as \"),\n            Self::Global => write!(f, \"global \"),\n            Self::Nonlocal => write!(f, \"nonlocal \"),\n        }\n    }\n}\n\nimpl Display for IndentLevel {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let spaces = match self {\n            Self::L0 => 0,\n            Self::L1 => 4,\n            Self::L2 => 8,\n            Self::L3 => 12,\n            Self::L4 => 16,\n        };\n        for _ in 0..spaces {\n            write!(f, \" \")?;\n        }\n        Ok(())\n    }\n}\n\n/// Wrapper for `Vec<Token>` with custom Debug that shows both tokens and generated code.\nstruct Tokens(Vec<Token>);\n\nimpl<'a> Arbitrary<'a> for Tokens {\n    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {\n        Vec::<Token>::arbitrary(u).map(Tokens)\n    }\n}\n\nimpl fmt::Debug for Tokens {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"Tokens\")\n            .field(\"tokens\", &self.0)\n            .field(\"code\", &self.to_code())\n            .finish()\n    }\n}\n\nimpl Tokens {\n    /// Convert the tokens to Python source code.\n    fn to_code(&self) -> String {\n        self.0.iter().map(|t| t.to_string()).collect()\n    }\n}\n\n/// Resource limits for fuzzing.\nfn fuzz_limits() -> LimitedTracker {\n    LimitedTracker::new(\n        ResourceLimits::new()\n            .max_allocations(10_000)\n            .max_memory(1024 * 1024) // 1 MB\n            .max_duration(Duration::from_millis(100)),\n    )\n}\n\nfuzz_target!(|tokens: Tokens| {\n    let code = tokens.to_code();\n\n    // Try to parse the code\n    let Ok(runner) = MontyRun::new(code, \"fuzz.py\", vec![]) else {\n        return; // Parse errors are expected\n    };\n\n    // Try to execute with resource limits\n    let _ = runner.run(vec![], fuzz_limits(), PrintWriter::Disabled);\n});\n"
  },
  {
    "path": "crates/monty/Cargo.toml",
    "content": "[package]\nname = \"monty\"\nreadme = \"../../README.md\"\nversion = { workspace = true }\nlicense = { workspace = true }\nrust-version = { workspace = true }\nedition = { workspace = true }\nauthors = { workspace = true }\ndescription = { workspace = true }\nkeywords = { workspace = true }\ncategories = { workspace = true }\nhomepage = { workspace = true }\nrepository = { workspace = true }\n\n[lib]\nname = \"monty\"\npath = \"src/lib.rs\"\n\n[dependencies]\nruff_python_parser = { workspace = true }\nruff_python_ast = { workspace = true }\nruff_text_size = { workspace = true }\nahash = { version = \"0.8.0\", features = [\"serde\"] }\nindexmap = { workspace = true }\nserde = { workspace = true }\npostcard = { workspace = true }\nstrum = { version = \"0.27\", features = [\"derive\"] }\nhashbrown = \"0.16\"\nnum-bigint = { workspace = true }\nnum-traits = { workspace = true }\nnum-integer = { workspace = true }\nsmallvec = { version = \"1.13\", features = [\"serde\"] }\nfancy-regex = \"0.17.0\"\nlibm = \"0.2\"\nitertools = \"0.14.0\"\n\n[features]\n# ref-count-return changes behavior to return information on reference counts to check they're correct\n# should be used for testing only\nref-count-return = []\n# ref-count-panic enables a Drop implementation on Value which catches heap allocated values that are dropped\n# without being dereferenced.\n# should be used for testing only\nref-count-panic = []\n\n[dev-dependencies]\npyo3 = { version = \"0.28\", features = [\"auto-initialize\"] }\n# Use codspeed-criterion-compat for CI benchmarks, real criterion for local flamegraphs\ncodspeed-criterion-compat = \"4.2.1\"\ncriterion = \"0.5\"\ndatatest-stable = \"0.2\"\nserde_json = \"1.0\"\npprof = { version = \"0.15\", features = [\"flamegraph\", \"criterion\"] }\nsimilar = \"2.7.0\"\n\n[build-dependencies]\npyo3-build-config = { version = \"0.28\", features = [\"resolve-config\"] }\n\n[[bench]]\nname = \"main\"\nharness = false\n\n[[test]]\nname = \"datatest_runner\"\nharness = false\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/monty/benches/main.rs",
    "content": "// Use codspeed-criterion-compat when running on CodSpeed (CI), real criterion otherwise (for flamegraphs)\n#[cfg(not(codspeed))]\nuse std::ffi::CString;\n\n#[cfg(codspeed)]\nuse codspeed_criterion_compat::{Bencher, Criterion, black_box, criterion_group, criterion_main};\n#[cfg(not(codspeed))]\nuse criterion::{Bencher, Criterion, black_box, criterion_group, criterion_main};\nuse monty::MontyRun;\n#[cfg(not(codspeed))]\nuse pprof::criterion::{Output, PProfProfiler};\n// CPython benchmarks are only run locally, not on CodSpeed CI (requires Python + pyo3 setup)\n#[cfg(not(codspeed))]\nuse pyo3::prelude::*;\n\n/// Runs a benchmark using the Monty interpreter.\n/// Parses once, then benchmarks repeated execution.\nfn run_monty(bench: &mut Bencher, code: &str, expected: i64) {\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let r = ex.run_no_limits(vec![]).unwrap();\n    let int_value: i64 = r.as_ref().try_into().unwrap();\n    assert_eq!(int_value, expected);\n\n    bench.iter(|| {\n        let r = ex.run_no_limits(vec![]).unwrap();\n        let int_value: i64 = r.as_ref().try_into().unwrap();\n        black_box(int_value);\n    });\n}\n\n/// Runs a benchmark using CPython.\n/// Wraps code in main(), parses once, then benchmarks repeated execution.\n#[cfg(not(codspeed))]\nfn run_cpython(bench: &mut Bencher, code: &str, expected: i64) {\n    Python::attach(|py| {\n        let wrapped = wrap_for_cpython(code);\n        let code_cstr = CString::new(wrapped).expect(\"Invalid C string in code\");\n        let fun: Py<PyAny> = PyModule::from_code(py, &code_cstr, c\"test.py\", c\"main\")\n            .unwrap()\n            .getattr(\"main\")\n            .unwrap()\n            .into();\n\n        let r_py = fun.call0(py).unwrap();\n        let r: i64 = r_py.extract(py).unwrap();\n        assert_eq!(r, expected);\n\n        bench.iter(|| {\n            let r_py = fun.call0(py).unwrap();\n            let r: i64 = r_py.extract(py).unwrap();\n            black_box(r);\n        });\n    });\n}\n\n/// Wraps code in a main() function for CPython execution.\n/// Indents each line and converts the last expression to a return statement.\n#[cfg(not(codspeed))]\nfn wrap_for_cpython(code: &str) -> String {\n    let mut lines: Vec<String> = Vec::new();\n    let mut last_expr = String::new();\n\n    for line in code.lines() {\n        // Skip test metadata comments\n        if line.starts_with(\"# Return=\") || line.starts_with(\"# Raise=\") || line.starts_with(\"# skip=\") {\n            continue;\n        }\n        // Track the last non-empty, non-comment line as potential return expression\n        let trimmed = line.trim();\n        if !trimmed.is_empty() && !trimmed.starts_with('#') {\n            last_expr = line.to_string();\n        }\n        lines.push(format!(\"    {line}\"));\n    }\n\n    // Replace last expression with return statement\n    if let Some(last) = lines.iter().rposition(|l| l.trim() == last_expr.trim()) {\n        lines[last] = format!(\"    return {}\", last_expr.trim());\n    }\n\n    format!(\"def main():\\n{}\", lines.join(\"\\n\"))\n}\n\nconst ADD_TWO: &str = \"1 + 2\";\n\nconst LIST_APPEND: &str = \"\na = []\na.append(42)\na[0]\n\";\n\nconst LOOP_MOD_13: &str = \"\nv = ''\nfor i in range(1_000):\n    if i % 13 == 0:\n        v += 'x'\nlen(v)\n\";\n\n/// Comprehensive benchmark exercising most supported Python features.\n/// Code is shared with test_cases/bench__kitchen_sink.py\nconst KITCHEN_SINK: &str = include_str!(\"../test_cases/bench__kitchen_sink.py\");\n\nconst FUNC_CALL_KWARGS: &str = \"\ndef add(a, b=2):\n    return a + b\n\nadd(a=1)\n\";\n\nconst LIST_APPEND_STR: &str = \"\na = []\nfor i in range(100_000):\n    a.append(str(i))\nlen(a)\n\";\n\nconst LIST_APPEND_INT: &str = \"\na = []\nfor i in range(100_000):\n    a.append(i)\nsum(a)\n\";\n\nconst FIB_25: &str = \"\ndef fib(n):\n    if n <= 1:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nfib(25)\n\";\n\n/// List comprehension benchmark - creates 1000 elements.\nconst LIST_COMP: &str = \"len([x * 2 for x in range(1000)])\";\n\n/// Dict comprehension benchmark - creates 500 unique keys (i // 2 deduplicates pairs).\nconst DICT_COMP: &str = \"len({i // 2: i * 2 for i in range(1000)})\";\n\n/// Empty tuple creation benchmark - creates 100,000 empty tuples in a list.\nconst EMPTY_TUPLES: &str = \"len([() for _ in range(100_000)])\";\n\n/// 2-tuple creation benchmark - creates 100,000 2-tuples in a list.\nconst PAIR_TUPLES: &str = \"len([(i, i + 1) for i in range(100_000)])\";\n\n/// Benchmarks end-to-end execution (parsing + running) using Monty.\n/// This is different from other benchmarks as it includes parsing in the loop.\nfn end_to_end_monty(bench: &mut Bencher) {\n    bench.iter(|| {\n        let ex = MontyRun::new(black_box(\"1 + 2\").to_owned(), \"test.py\", vec![]).unwrap();\n        let r = ex.run_no_limits(vec![]).unwrap();\n        let int_value: i64 = r.as_ref().try_into().unwrap();\n        black_box(int_value);\n    });\n}\n\n/// Benchmarks end-to-end execution (parsing + running) using CPython.\n/// This is different from other benchmarks as it includes parsing in the loop.\n#[cfg(not(codspeed))]\nfn end_to_end_cpython(bench: &mut Bencher) {\n    Python::attach(|py| {\n        bench.iter(|| {\n            let fun: Py<PyAny> =\n                PyModule::from_code(py, black_box(c\"def main():\\n  return 1 + 2\"), c\"test.py\", c\"main\")\n                    .unwrap()\n                    .getattr(\"main\")\n                    .unwrap()\n                    .into();\n            let r_py = fun.call0(py).unwrap();\n            let r: i64 = r_py.extract(py).unwrap();\n            black_box(r);\n        });\n    });\n}\n\n/// Configures all benchmarks in a single group.\nfn criterion_benchmark(c: &mut Criterion) {\n    c.bench_function(\"add_two__monty\", |b| run_monty(b, ADD_TWO, 3));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"add_two__cpython\", |b| run_cpython(b, ADD_TWO, 3));\n\n    c.bench_function(\"list_append__monty\", |b| run_monty(b, LIST_APPEND, 42));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"list_append__cpython\", |b| run_cpython(b, LIST_APPEND, 42));\n\n    c.bench_function(\"loop_mod_13__monty\", |b| run_monty(b, LOOP_MOD_13, 77));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"loop_mod_13__cpython\", |b| run_cpython(b, LOOP_MOD_13, 77));\n\n    c.bench_function(\"end_to_end__monty\", end_to_end_monty);\n    #[cfg(not(codspeed))]\n    c.bench_function(\"end_to_end__cpython\", end_to_end_cpython);\n\n    c.bench_function(\"kitchen_sink__monty\", |b| run_monty(b, KITCHEN_SINK, 373));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"kitchen_sink__cpython\", |b| run_cpython(b, KITCHEN_SINK, 373));\n\n    c.bench_function(\"func_call_kwargs__monty\", |b| run_monty(b, FUNC_CALL_KWARGS, 3));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"func_call_kwargs__cpython\", |b| run_cpython(b, FUNC_CALL_KWARGS, 3));\n\n    c.bench_function(\"list_append_str__monty\", |b| run_monty(b, LIST_APPEND_STR, 100_000));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"list_append_str__cpython\", |b| run_cpython(b, LIST_APPEND_STR, 100_000));\n\n    c.bench_function(\"list_append_int__monty\", |b| {\n        run_monty(b, LIST_APPEND_INT, 4_999_950_000);\n    });\n    #[cfg(not(codspeed))]\n    c.bench_function(\"list_append_int__cpython\", |b| {\n        run_cpython(b, LIST_APPEND_INT, 4_999_950_000);\n    });\n\n    c.bench_function(\"fib__monty\", |b| run_monty(b, FIB_25, 75_025));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"fib__cpython\", |b| run_cpython(b, FIB_25, 75_025));\n\n    c.bench_function(\"list_comp__monty\", |b| run_monty(b, LIST_COMP, 1000));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"list_comp__cpython\", |b| run_cpython(b, LIST_COMP, 1000));\n\n    c.bench_function(\"dict_comp__monty\", |b| run_monty(b, DICT_COMP, 500));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"dict_comp__cpython\", |b| run_cpython(b, DICT_COMP, 500));\n\n    c.bench_function(\"empty_tuples__monty\", |b| run_monty(b, EMPTY_TUPLES, 100_000));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"empty_tuples__cpython\", |b| run_cpython(b, EMPTY_TUPLES, 100_000));\n\n    c.bench_function(\"pair_tuples__monty\", |b| run_monty(b, PAIR_TUPLES, 100_000));\n    #[cfg(not(codspeed))]\n    c.bench_function(\"pair_tuples__cpython\", |b| run_cpython(b, PAIR_TUPLES, 100_000));\n}\n\n// Use pprof flamegraph profiler when running locally (not on CodSpeed)\n#[cfg(not(codspeed))]\ncriterion_group!(\n    name = benches;\n    config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));\n    targets = criterion_benchmark\n);\n\n// Use default config when running on CodSpeed (pprof's Profiler trait is incompatible)\n#[cfg(codspeed)]\ncriterion_group!(benches, criterion_benchmark);\n\ncriterion_main!(benches);\n"
  },
  {
    "path": "crates/monty/build.rs",
    "content": "fn main() {\n    // This ensures that tests can find the libpython shared library at runtime, even if it's not on\n    // the system library path. This makes running tests much easier on e.g. Linux with a uv venv.\n    //\n    // This is technically a bit wasteful because the main `lib` doesn't need this, just tests, but it\n    // won't affect downstream executables other than requiring them to have a valid Python in their system.\n    //\n    // If that becomes a big problem, we can rethink.\n    pyo3_build_config::add_libpython_rpath_link_args();\n}\n"
  },
  {
    "path": "crates/monty/src/args.rs",
    "content": "use std::vec::IntoIter;\n\nuse crate::{\n    MontyObject, ResourceTracker,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunError, RunResult, SimpleException},\n    expressions::{ExprLoc, Identifier},\n    heap::{ContainsHeap, DropWithHeap, Heap, HeapGuard},\n    intern::{Interns, StringId},\n    parse::ParseError,\n    types::{Dict, dict::DictIntoIter},\n    value::Value,\n};\n\n/// Type for method call arguments.\n///\n/// Uses specific variants for common cases (0-2 arguments).\n/// Most Python method calls have at most 2 arguments, so this optimization\n/// eliminates the Vec heap allocation overhead for the vast majority of calls.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum ArgValues {\n    Empty,\n    One(Value),\n    Two(Value, Value),\n    Kwargs(KwargsValues),\n    ArgsKargs { args: Vec<Value>, kwargs: KwargsValues },\n}\n\nimpl ArgValues {\n    /// Checks that zero arguments were passed.\n    ///\n    /// On error, properly drops all contained values to maintain reference counts.\n    pub fn check_zero_args(self, name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<()> {\n        match self {\n            Self::Empty => Ok(()),\n            other => {\n                let count = other.count();\n                other.drop_with_heap(heap);\n                Err(ExcType::type_error_no_args(name, count))\n            }\n        }\n    }\n\n    /// Checks that exactly one positional argument was passed, returning it.\n    ///\n    /// On error, properly drops all contained values to maintain reference counts.\n    pub fn get_one_arg(self, name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        match self {\n            Self::One(a) => Ok(a),\n            other => {\n                let count = other.count();\n                other.drop_with_heap(heap);\n                Err(ExcType::type_error_arg_count(name, 1, count))\n            }\n        }\n    }\n\n    /// Checks that exactly two positional arguments were passed, returning them as a tuple.\n    ///\n    /// On error, properly drops all contained values to maintain reference counts.\n    pub fn get_two_args(self, name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<(Value, Value)> {\n        match self {\n            Self::Two(a1, a2) => Ok((a1, a2)),\n            other => {\n                let count = other.count();\n                other.drop_with_heap(heap);\n                Err(ExcType::type_error_arg_count(name, 2, count))\n            }\n        }\n    }\n\n    /// Checks that one or two arguments were passed, returning them as a tuple.\n    ///\n    /// On error, properly drops all contained values to maintain reference counts.\n    pub fn get_one_two_args(\n        self,\n        name: &str,\n        heap: &mut Heap<impl ResourceTracker>,\n    ) -> RunResult<(Value, Option<Value>)> {\n        match self {\n            Self::One(a) => Ok((a, None)),\n            Self::Two(a1, a2) => Ok((a1, Some(a2))),\n            other => {\n                let count = other.count();\n                other.drop_with_heap(heap);\n                if count == 0 {\n                    Err(ExcType::type_error_at_least(name, 1, count))\n                } else {\n                    Err(ExcType::type_error_at_most(name, 2, count))\n                }\n            }\n        }\n    }\n\n    /// Checks that zero or one argument was passed, returning the optional value.\n    ///\n    /// On error, properly drops all contained values to maintain reference counts.\n    pub fn get_zero_one_arg(self, name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Option<Value>> {\n        match self {\n            Self::Empty => Ok(None),\n            Self::One(a) => Ok(Some(a)),\n            other => {\n                let count = other.count();\n                other.drop_with_heap(heap);\n                Err(ExcType::type_error_at_most(name, 1, count))\n            }\n        }\n    }\n\n    /// Checks that zero, one, or two arguments were passed.\n    ///\n    /// Returns (None, None) for 0 args, (Some(a), None) for 1 arg, (Some(a), Some(b)) for 2 args.\n    /// On error, properly drops all contained values to maintain reference counts.\n    pub fn get_zero_one_two_args(\n        self,\n        name: &str,\n        heap: &mut Heap<impl ResourceTracker>,\n    ) -> RunResult<(Option<Value>, Option<Value>)> {\n        match self {\n            Self::Empty => Ok((None, None)),\n            Self::One(a) => Ok((Some(a), None)),\n            Self::Two(a, b) => Ok((Some(a), Some(b))),\n            other => {\n                let count = other.count();\n                other.drop_with_heap(heap);\n                Err(ExcType::type_error_at_most(name, 2, count))\n            }\n        }\n    }\n\n    /// Extracts a keyword-only pair by name.\n    ///\n    /// Validates that no positional arguments are provided and only the specified\n    /// keyword arguments are present. Returns `(None, None)` when neither keyword\n    /// is provided.\n    ///\n    /// # Arguments\n    /// * `method_name` - Method name for error messages (e.g., \"list.sort\")\n    /// * `kwarg1` - Name of the first keyword argument\n    /// * `kwarg2` - Name of the second keyword argument\n    ///\n    /// # Errors\n    /// Returns an error if:\n    /// - Any positional arguments are provided\n    /// - A keyword argument other than `kwarg1` or `kwarg2` is provided\n    /// - A keyword is not a string\n    pub fn extract_keyword_only_pair(\n        self,\n        method_name: &str,\n        kwarg1: &str,\n        kwarg2: &str,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> RunResult<(Option<Value>, Option<Value>)> {\n        let (pos, kwargs) = self.into_parts();\n        defer_drop!(pos, heap);\n\n        // Check no positional arguments\n        if pos.len() > 0 {\n            kwargs.drop_with_heap(heap);\n            return Err(ExcType::type_error_no_args(method_name, 1));\n        }\n\n        kwargs.parse_named_kwargs_pair(method_name, kwarg1, kwarg2, heap, interns, |method_name, key_str| {\n            ExcType::type_error(format!(\n                \"'{key_str}' is an invalid keyword argument for {method_name}()\"\n            ))\n        })\n    }\n\n    /// Prepends a value as the first positional argument.\n    ///\n    /// Used to insert `self` when dispatching dataclass method calls to the host.\n    /// The dataclass instance becomes the first arg so the host can reconstruct\n    /// the original object and call the method on it.\n    pub fn prepend(self, value: Value) -> Self {\n        match self {\n            Self::Empty => Self::One(value),\n            Self::One(a) => Self::Two(value, a),\n            Self::Two(a, b) => Self::ArgsKargs {\n                args: vec![value, a, b],\n                kwargs: KwargsValues::Empty,\n            },\n            Self::Kwargs(kw) => Self::ArgsKargs {\n                args: vec![value],\n                kwargs: kw,\n            },\n            Self::ArgsKargs { mut args, kwargs } => {\n                args.insert(0, value);\n                Self::ArgsKargs { args, kwargs }\n            }\n        }\n    }\n\n    /// Splits into positional iterator and keyword values without allocating\n    /// for the common One/Two cases.\n    pub fn into_parts(self) -> (ArgPosIter, KwargsValues) {\n        match self {\n            Self::Empty => (ArgPosIter::Empty, KwargsValues::Empty),\n            Self::One(v) => (ArgPosIter::One(v), KwargsValues::Empty),\n            Self::Two(v1, v2) => (ArgPosIter::Two([v1, v2]), KwargsValues::Empty),\n            Self::Kwargs(kwargs) => (ArgPosIter::Empty, kwargs),\n            Self::ArgsKargs { args, kwargs } => (ArgPosIter::Vec(args.into_iter()), kwargs),\n        }\n    }\n\n    /// Variant of [`into_parts()`](Self::into_parts) that accepts no kwargs, returning an error if any are present.\n    pub fn into_pos_only(self, method_name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<ArgPosIter> {\n        match self {\n            Self::Empty => Ok(ArgPosIter::Empty),\n            Self::One(v) => Ok(ArgPosIter::One(v)),\n            Self::Two(v1, v2) => Ok(ArgPosIter::Two([v1, v2])),\n            Self::Kwargs(kwargs) => {\n                if kwargs.is_empty() {\n                    Ok(ArgPosIter::Empty)\n                } else {\n                    Err(Self::unexpected_kwargs_error(kwargs, method_name, heap))\n                }\n            }\n            Self::ArgsKargs { args, kwargs } => {\n                if kwargs.is_empty() {\n                    Ok(ArgPosIter::Vec(args.into_iter()))\n                } else {\n                    args.drop_with_heap(heap);\n                    Err(Self::unexpected_kwargs_error(kwargs, method_name, heap))\n                }\n            }\n        }\n    }\n\n    #[cold]\n    fn unexpected_kwargs_error(\n        kwargs: KwargsValues,\n        method_name: &str,\n        heap: &mut Heap<impl ResourceTracker>,\n    ) -> RunError {\n        kwargs.drop_with_heap(heap);\n        ExcType::type_error_no_kwargs(method_name)\n    }\n\n    /// Converts the arguments into a Vec of MontyObjects.\n    ///\n    /// This is used when passing arguments to external functions.\n    pub fn into_py_objects(\n        self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> (Vec<MontyObject>, Vec<(MontyObject, MontyObject)>) {\n        match self {\n            Self::Empty => (vec![], vec![]),\n            Self::One(a) => (vec![MontyObject::new(a, vm)], vec![]),\n            Self::Two(a1, a2) => (vec![MontyObject::new(a1, vm), MontyObject::new(a2, vm)], vec![]),\n            Self::Kwargs(kwargs) => (vec![], kwargs.into_py_objects(vm)),\n            Self::ArgsKargs { args, kwargs } => (\n                args.into_iter().map(|v| MontyObject::new(v, vm)).collect(),\n                kwargs.into_py_objects(vm),\n            ),\n        }\n    }\n\n    /// Returns the number of positional arguments.\n    ///\n    /// For `Kwargs` returns 0, for `ArgsKargs` returns only the positional args count.\n    fn count(&self) -> usize {\n        match self {\n            Self::Empty => 0,\n            Self::One(_) => 1,\n            Self::Two(_, _) => 2,\n            Self::Kwargs(_) => 0,\n            Self::ArgsKargs { args, .. } => args.len(),\n        }\n    }\n}\n\nimpl DropWithHeap for ArgValues {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        match self {\n            Self::Empty => {}\n            Self::One(v) => v.drop_with_heap(heap),\n            Self::Two(v1, v2) => {\n                v1.drop_with_heap(heap);\n                v2.drop_with_heap(heap);\n            }\n            Self::Kwargs(kwargs) => {\n                kwargs.drop_with_heap(heap);\n            }\n            Self::ArgsKargs { args, kwargs } => {\n                args.drop_with_heap(heap);\n                kwargs.drop_with_heap(heap);\n            }\n        }\n    }\n}\n\n/// Iterator over positional arguments without allocation.\n///\n/// Supports iterating over `ArgValues::One/Two` without converting to Vec.\n/// This iterator must be fully consumed OR explicitly dropped with\n/// `drop_remaining_with_heap()` to maintain correct reference counts.\n///\n/// The iterator yields values by ownership transfer. Once a value is yielded,\n/// the caller is responsible for either using it or calling `drop_with_heap()` on it.\npub(crate) enum ArgPosIter {\n    Empty,\n    One(Value),\n    Two([Value; 2]),\n    Vec(IntoIter<Value>),\n}\n\nimpl ArgPosIter {\n    /// Returns a slice of the remaining positional arguments without consuming them.\n    pub fn as_slice(&self) -> &[Value] {\n        match self {\n            Self::Empty => &[],\n            Self::One(v) => std::slice::from_ref(v),\n            Self::Two(array) => array.as_slice(),\n            Self::Vec(iter) => iter.as_slice(),\n        }\n    }\n}\n\nimpl Iterator for ArgPosIter {\n    type Item = Value;\n\n    #[inline]\n    fn next(&mut self) -> Option<Value> {\n        match self {\n            Self::Empty => None,\n            Self::One(_) => {\n                let Self::One(v) = std::mem::replace(self, Self::Empty) else {\n                    unreachable!()\n                };\n                Some(v)\n            }\n            Self::Two(_) => {\n                let Self::Two([v1, v2]) = std::mem::replace(self, Self::Empty) else {\n                    unreachable!()\n                };\n                *self = Self::One(v2);\n                Some(v1)\n            }\n            Self::Vec(iter) => iter.next(),\n        }\n    }\n\n    #[inline]\n    fn size_hint(&self) -> (usize, Option<usize>) {\n        match self {\n            Self::Empty => (0, Some(0)),\n            Self::One(_) => (1, Some(1)),\n            Self::Two(_) => (2, Some(2)),\n            Self::Vec(iter) => iter.size_hint(),\n        }\n    }\n}\n\nimpl ExactSizeIterator for ArgPosIter {}\n\nimpl DropWithHeap for ArgPosIter {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        match self {\n            Self::Empty => {}\n            Self::One(v1) => v1.drop_with_heap(heap),\n            Self::Two(v12) => v12.drop_with_heap(heap),\n            Self::Vec(iter) => iter.drop_with_heap(heap),\n        }\n    }\n}\n\n/// Type for keyword arguments.\n///\n/// Used to capture both the case of inline keyword arguments `foo(foo=1, bar=2)`\n/// and the case of a dictionary passed as a single argument `foo(**kwargs)`.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum KwargsValues {\n    Empty,\n    Inline(Vec<(StringId, Value)>),\n    Dict(Dict),\n}\n\nimpl KwargsValues {\n    /// Returns the number of keyword arguments.\n    #[must_use]\n    pub fn len(&self) -> usize {\n        match self {\n            Self::Empty => 0,\n            Self::Inline(kvs) => kvs.len(),\n            Self::Dict(dict) => dict.len(),\n        }\n    }\n\n    /// Returns true if there are no keyword arguments.\n    #[must_use]\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    /// Converts the arguments into a Vec of MontyObjects.\n    ///\n    /// This is used when passing arguments to external functions.\n    fn into_py_objects(self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Vec<(MontyObject, MontyObject)> {\n        match self {\n            Self::Empty => vec![],\n            Self::Inline(kvs) => kvs\n                .into_iter()\n                .map(|(k, v)| {\n                    let key = MontyObject::String(vm.interns.get_str(k).to_owned());\n                    let value = MontyObject::new(v, vm);\n                    (key, value)\n                })\n                .collect(),\n            Self::Dict(dict) => dict\n                .into_iter()\n                .map(|(k, v)| (MontyObject::new(k, vm), MontyObject::new(v, vm)))\n                .collect(),\n        }\n    }\n\n    /// Helper for functions which do not yet support kwargs, returns an `Err` if there are kwargs.\n    pub fn not_supported_yet(self, method_name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<()> {\n        if self.is_empty() {\n            Ok(())\n        } else {\n            self.drop_with_heap(heap);\n            Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"{method_name}() does not support keyword arguments yet\"),\n            )\n            .into())\n        }\n    }\n\n    /// Parses a fixed pair of named keyword arguments with duplicate checking.\n    ///\n    /// This helper is intentionally narrow: it covers the common builtin/method\n    /// pattern of accepting a tiny fixed keyword surface such as `key/default`\n    /// or `key/reverse`, while leaving positional-argument validation and any\n    /// post-processing to the caller.\n    ///\n    /// `unexpected_keyword` formats the call-site-specific error for keywords\n    /// other than `kwarg1` and `kwarg2`.\n    pub fn parse_named_kwargs_pair(\n        self,\n        func_name: &str,\n        kwarg1: &str,\n        kwarg2: &str,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n        unexpected_keyword: impl Fn(&str, &str) -> RunError,\n    ) -> RunResult<(Option<Value>, Option<Value>)> {\n        let kwargs = self.into_iter();\n        defer_drop_mut!(kwargs, heap);\n\n        // Guards are reversed so that destructure can pull them.\n        let mut val2_guard = HeapGuard::new(None::<Value>, heap);\n        let (val2, heap) = val2_guard.as_parts_mut();\n        let mut val1_guard = HeapGuard::new(None::<Value>, heap);\n        let (val1, heap) = val1_guard.as_parts_mut();\n\n        for (key, value) in kwargs {\n            defer_drop!(key, heap);\n            let mut value = HeapGuard::new(value, heap);\n\n            let Some(keyword_name) = key.as_either_str(value.heap()) else {\n                return Err(ExcType::type_error_kwargs_nonstring_key());\n            };\n\n            let key_str = keyword_name.as_str(interns);\n            if key_str == kwarg1 {\n                if val1.is_some() {\n                    return Err(ExcType::type_error_multiple_values(func_name, key_str));\n                }\n                *val1 = Some(value.into_inner());\n            } else if key_str == kwarg2 {\n                if val2.is_some() {\n                    return Err(ExcType::type_error_multiple_values(func_name, key_str));\n                }\n                *val2 = Some(value.into_inner());\n            } else {\n                return Err(unexpected_keyword(func_name, key_str));\n            }\n        }\n\n        Ok((val1_guard.into_inner(), val2_guard.into_inner()))\n    }\n}\n\nimpl DropWithHeap for KwargsValues {\n    /// Properly drops all values in the arguments, decrementing reference counts.\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        match self {\n            Self::Empty => {}\n            Self::Inline(kvs) => {\n                for (_, v) in kvs {\n                    v.drop_with_heap(heap);\n                }\n            }\n            Self::Dict(dict) => {\n                for (k, v) in dict {\n                    k.drop_with_heap(heap);\n                    v.drop_with_heap(heap);\n                }\n            }\n        }\n    }\n}\n\nimpl IntoIterator for KwargsValues {\n    type Item = (Value, Value);\n    type IntoIter = KwargsValuesIter;\n\n    fn into_iter(self) -> Self::IntoIter {\n        match self {\n            Self::Empty => KwargsValuesIter::Empty,\n            Self::Inline(kvs) => KwargsValuesIter::Inline(kvs.into_iter()),\n            Self::Dict(dict) => KwargsValuesIter::Dict(dict.into_iter()),\n        }\n    }\n}\n\n/// Iterator over keyword argument (key, value) pairs.\n///\n/// For `Inline` kwargs, converts `StringId` keys to `Value::InternString`.\n/// For `Dict` kwargs, iterates directly over the dict's entries without\n/// intermediate allocation.\npub(crate) enum KwargsValuesIter {\n    Empty,\n    Inline(IntoIter<(StringId, Value)>),\n    Dict(DictIntoIter),\n}\n\nimpl Iterator for KwargsValuesIter {\n    type Item = (Value, Value);\n\n    fn next(&mut self) -> Option<Self::Item> {\n        match self {\n            Self::Empty => None,\n            Self::Inline(iter) => iter.next().map(|(k, v)| (Value::InternString(k), v)),\n            Self::Dict(iter) => iter.next(),\n        }\n    }\n\n    fn size_hint(&self) -> (usize, Option<usize>) {\n        match self {\n            Self::Empty => (0, Some(0)),\n            Self::Inline(iter) => iter.size_hint(),\n            Self::Dict(iter) => iter.size_hint(),\n        }\n    }\n}\n\nimpl ExactSizeIterator for KwargsValuesIter {}\n\nimpl DropWithHeap for KwargsValuesIter {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        match self {\n            Self::Empty => {}\n            Self::Inline(iter) => {\n                for (_, v) in iter {\n                    v.drop_with_heap(heap);\n                }\n            }\n            Self::Dict(iter) => {\n                for (k, v) in iter {\n                    k.drop_with_heap(heap);\n                    v.drop_with_heap(heap);\n                }\n            }\n        }\n    }\n}\n\n/// A keyword argument in a function call expression.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct Kwarg {\n    pub key: Identifier,\n    pub value: ExprLoc,\n}\n\n/// A positional argument item in a generalized function call (PEP 448).\n///\n/// Used in `ArgExprs::GeneralizedCall` when a call has multiple `*unpacks`\n/// or positional arguments after a `*unpack`. Each item is either a plain\n/// value or a `*expr` iterable to be unpacked into the argument tuple.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) enum CallArg {\n    /// A plain positional argument.\n    Value(ExprLoc),\n    /// A `*expr` unpack — the iterable is spread into consecutive arguments.\n    Unpack(ExprLoc),\n}\n\n/// A keyword argument item in a generalized function call (PEP 448).\n///\n/// Used in `ArgExprs::GeneralizedCall` when a call has multiple `**unpacks`\n/// or named kwargs interspersed with `**unpacks`. Duplicate keys from any\n/// combination raise `TypeError` (both `f(**a, **b)` with shared keys and\n/// `f(x=1, **{'x': 2})` are errors). This is enforced by `DictMerge` in\n/// the compiler.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) enum CallKwarg {\n    /// A named keyword argument: `key=value`.\n    Named(Kwarg),\n    /// A `**expr` unpack — the mapping's entries are merged into kwargs.\n    Unpack(ExprLoc),\n}\n\n/// Expressions that make up a function call's arguments.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum ArgExprs {\n    Empty,\n    One(ExprLoc),\n    Two(ExprLoc, ExprLoc),\n    Args(Vec<ExprLoc>),\n    Kwargs(Vec<Kwarg>),\n    ArgsKargs {\n        args: Option<Vec<ExprLoc>>,\n        var_args: Option<ExprLoc>,\n        kwargs: Option<Vec<Kwarg>>,\n        var_kwargs: Option<ExprLoc>,\n    },\n    /// Generalized call with PEP 448 unpacking.\n    ///\n    /// Used when a call has multiple `*args` unpacks, positional arguments\n    /// after a `*unpack`, or multiple `**kwargs` unpacks. The compiler\n    /// builds the args tuple incrementally using `BuildList(0)` +\n    /// `ListAppend`/`ListExtend` + `ListToTuple`, and the kwargs dict\n    /// using `BuildDict(0)` + `DictMerge` (which raises `TypeError` on\n    /// duplicate keys).\n    GeneralizedCall {\n        args: Vec<CallArg>,\n        kwargs: Vec<CallKwarg>,\n    },\n}\n\nimpl ArgExprs {\n    /// Creates a `GeneralizedCall` for PEP 448 calls with multiple unpacks.\n    ///\n    /// Use this when a function call has multiple `*args` unpacks, positional\n    /// arguments after a `*unpack`, or multiple `**kwargs` unpacks. The compiler\n    /// will emit `BuildList(0)` + `ListAppend`/`ListExtend` + `ListToTuple` for\n    /// the args tuple, and `BuildDict(0)` + `DictMerge` for the kwargs dict.\n    pub(crate) fn new_generalized(args: Vec<CallArg>, kwargs: Vec<CallKwarg>) -> Self {\n        Self::GeneralizedCall { args, kwargs }\n    }\n\n    /// Creates a new `ArgExprs` with optional `*args` and `**kwargs` unpacking expressions.\n    ///\n    /// This is used when parsing function calls that may include `*expr` / `**expr`\n    /// syntax for unpacking iterables or mappings into arguments.\n    pub fn new_with_var_kwargs(\n        args: Vec<ExprLoc>,\n        var_args: Option<ExprLoc>,\n        kwargs: Vec<Kwarg>,\n        var_kwargs: Option<ExprLoc>,\n    ) -> Self {\n        // Full generality requires ArgsKargs when we have unpacking or mixed arg/kwarg usage\n        if var_args.is_some() || var_kwargs.is_some() || (!kwargs.is_empty() && !args.is_empty()) {\n            Self::ArgsKargs {\n                args: if args.is_empty() { None } else { Some(args) },\n                var_args,\n                kwargs: if kwargs.is_empty() { None } else { Some(kwargs) },\n                var_kwargs,\n            }\n        } else if !kwargs.is_empty() {\n            Self::Kwargs(kwargs)\n        } else if args.len() > 2 {\n            Self::Args(args)\n        } else {\n            let mut iter = args.into_iter();\n            if let Some(first) = iter.next() {\n                if let Some(second) = iter.next() {\n                    Self::Two(first, second)\n                } else {\n                    Self::One(first)\n                }\n            } else {\n                Self::Empty\n            }\n        }\n    }\n\n    /// Applies a transformation function to all `ExprLoc` elements in the args.\n    ///\n    /// This is used during the preparation phase to recursively prepare all\n    /// argument expressions before execution.\n    pub fn prepare_args(\n        &mut self,\n        mut f: impl FnMut(ExprLoc) -> Result<ExprLoc, ParseError>,\n    ) -> Result<(), ParseError> {\n        // Swap self with Empty to take ownership, then rebuild\n        let taken = std::mem::replace(self, Self::Empty);\n        *self = match taken {\n            Self::Empty => Self::Empty,\n            Self::One(arg) => Self::One(f(arg)?),\n            Self::Two(arg1, arg2) => Self::Two(f(arg1)?, f(arg2)?),\n            Self::Args(args) => Self::Args(args.into_iter().map(&mut f).collect::<Result<Vec<_>, _>>()?),\n            Self::Kwargs(kwargs) => Self::Kwargs(\n                kwargs\n                    .into_iter()\n                    .map(|kwarg| {\n                        Ok(Kwarg {\n                            key: kwarg.key,\n                            value: f(kwarg.value)?,\n                        })\n                    })\n                    .collect::<Result<Vec<_>, ParseError>>()?,\n            ),\n            Self::ArgsKargs {\n                args,\n                var_args,\n                kwargs,\n                var_kwargs,\n            } => {\n                let args = args\n                    .map(|a| a.into_iter().map(&mut f).collect::<Result<Vec<_>, ParseError>>())\n                    .transpose()?;\n                let var_args = var_args.map(&mut f).transpose()?;\n                let kwargs = kwargs\n                    .map(|k| {\n                        k.into_iter()\n                            .map(|kwarg| {\n                                Ok(Kwarg {\n                                    key: kwarg.key,\n                                    value: f(kwarg.value)?,\n                                })\n                            })\n                            .collect::<Result<Vec<_>, ParseError>>()\n                    })\n                    .transpose()?;\n                let var_kwargs = var_kwargs.map(&mut f).transpose()?;\n                Self::ArgsKargs {\n                    args,\n                    var_args,\n                    kwargs,\n                    var_kwargs,\n                }\n            }\n            Self::GeneralizedCall { args, kwargs } => {\n                let args = args\n                    .into_iter()\n                    .map(|arg| match arg {\n                        CallArg::Value(e) => Ok(CallArg::Value(f(e)?)),\n                        CallArg::Unpack(e) => Ok(CallArg::Unpack(f(e)?)),\n                    })\n                    .collect::<Result<Vec<_>, ParseError>>()?;\n                let kwargs = kwargs\n                    .into_iter()\n                    .map(|kwarg| match kwarg {\n                        CallKwarg::Named(kw) => Ok(CallKwarg::Named(Kwarg {\n                            key: kw.key,\n                            value: f(kw.value)?,\n                        })),\n                        CallKwarg::Unpack(e) => Ok(CallKwarg::Unpack(f(e)?)),\n                    })\n                    .collect::<Result<Vec<_>, ParseError>>()?;\n                Self::GeneralizedCall { args, kwargs }\n            }\n        };\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/asyncio.rs",
    "content": "//! Async/await support types for Monty.\n//!\n//! This module contains all async-related types including coroutines, futures,\n//! and task identifiers. The host acts as the event loop - external function\n//! calls return `ExternalFuture` objects that can be awaited.\n\nuse crate::{heap::HeapId, intern::FunctionId, value::Value};\n\n/// Unique identifier for external function calls.\n///\n/// Sequential integers allocated by the scheduler. Used to correlate\n/// external function calls with their results when the host resolves them.\n/// The counter always increments, even for sync resolution, to keep IDs unique.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct CallId(u32);\n\nimpl CallId {\n    /// Creates a new CallId from a raw value.\n    #[inline]\n    pub fn new(id: u32) -> Self {\n        Self(id)\n    }\n\n    /// Returns the raw u32 value.\n    #[inline]\n    pub fn raw(self) -> u32 {\n        self.0\n    }\n}\n\n/// Unique identifier for an async task.\n///\n/// Sequential integers allocated by the scheduler. Task 0 is always the main task\n/// which uses the VM's stack/frames directly. Spawned tasks (1+) store their own context,\n/// hence `TaskId::default()` is the main task.\n#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct TaskId(u32);\n\nimpl TaskId {\n    /// Creates a new TaskId from a raw value.\n    #[inline]\n    pub fn new(id: u32) -> Self {\n        Self(id)\n    }\n\n    /// Returns the raw u32 value.\n    #[inline]\n    pub fn raw(self) -> u32 {\n        self.0\n    }\n\n    /// Returns true if this is the main task (task 0).\n    #[inline]\n    pub fn is_main(self) -> bool {\n        self.0 == 0\n    }\n}\n\n/// Coroutine execution state (single-shot semantics).\n///\n/// Coroutines in Monty follow single-shot semantics - they can only be awaited once.\n/// This differs from Python generators which can be resumed multiple times.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub(crate) enum CoroutineState {\n    /// Coroutine has been created but not yet awaited.\n    New,\n    /// Coroutine is currently executing (has been awaited).\n    Running,\n    /// Coroutine has finished execution.\n    Completed,\n}\n\n/// A coroutine object representing an async function call result.\n///\n/// Created when an `async def` function is called. Argument binding happens at call time;\n/// awaiting the coroutine starts execution. Coroutines use single-shot semantics -\n/// they can only be awaited once.\n///\n/// # Namespace Layout\n///\n/// The `namespace` vector is pre-sized to match the function's namespace size and contains:\n/// ```text\n/// [params...][cell_vars...][free_vars...][locals...]\n/// ```\n/// - Parameter slots are filled with bound argument values at call time\n/// - Cell/free var slots contain `Value::Ref` to captured cells\n/// - Local slots start as `Value::Undefined`\n///\n/// When the coroutine is awaited, these values are pushed onto the VM's stack\n/// as inline locals, and a new frame is pushed to execute the async function body.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct Coroutine {\n    /// The async function to execute.\n    pub func_id: FunctionId,\n    /// Pre-bound namespace values (sized to function namespace).\n    /// Contains bound parameters, captured cells, and uninitialized locals.\n    pub namespace: Vec<Value>,\n    /// Current execution state.\n    pub state: CoroutineState,\n}\nimpl Coroutine {\n    /// Creates a new coroutine for an async function call.\n    ///\n    /// # Arguments\n    /// * `func_id` - The async function to execute\n    /// * `namespace` - Pre-bound namespace with parameters and captured variables\n    pub fn new(func_id: FunctionId, namespace: Vec<Value>) -> Self {\n        Self {\n            func_id,\n            namespace,\n            state: CoroutineState::New,\n        }\n    }\n}\n\n/// An item that can be gathered - either a coroutine or an external future.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) enum GatherItem {\n    /// A coroutine to spawn as a task.\n    Coroutine(HeapId),\n    /// An external future to wait for resolution.\n    ExternalFuture(CallId),\n}\n\n/// A gather() result tracking multiple coroutines/tasks and external futures.\n///\n/// Created by `asyncio.gather(*awaitables)`. Does NOT spawn tasks immediately -\n/// tasks are spawned when the GatherFuture is awaited in Await.\n///\n/// # Lifecycle\n///\n/// 1. **Creation**: `gather(coro1, coro2, ...)` stores coroutine HeapIds and external CallIds\n/// 2. **Await**: `await gather_future` spawns tasks and blocks the current task\n/// 3. **Completion**: As tasks/futures complete, results are stored in order\n/// 4. **Return**: When all items complete, returns list of results\n///\n/// # Error Handling\n///\n/// On any task failure, sibling tasks are cancelled and the exception propagates\n/// to the task that awaited the gather.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct GatherFuture {\n    /// Items to gather (coroutines or external futures).\n    pub items: Vec<GatherItem>,\n    /// TaskIds of spawned tasks (only for coroutine items, set when awaited).\n    /// Length matches the number of Coroutine items.\n    pub task_ids: Vec<TaskId>,\n    /// Results from each item, in order (filled as items complete).\n    /// Indices align with `items`.\n    pub results: Vec<Option<Value>>,\n    /// Task waiting on this gather (set when awaited).\n    pub waiter: Option<TaskId>,\n    /// CallIds of external futures we're waiting on.\n    /// Used to check if all external futures have resolved.\n    pub pending_calls: Vec<CallId>,\n}\n\nimpl GatherFuture {\n    /// Creates a new GatherFuture with the given items.\n    ///\n    /// # Arguments\n    /// * `items` - Coroutines or external futures to run concurrently\n    pub fn new(items: Vec<GatherItem>) -> Self {\n        let count = items.len();\n        Self {\n            items,\n            task_ids: Vec::new(),\n            results: (0..count).map(|_| None).collect(),\n            waiter: None,\n            pending_calls: Vec::new(),\n        }\n    }\n\n    /// Returns the number of items to gather.\n    #[inline]\n    pub fn item_count(&self) -> usize {\n        self.items.len()\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/abs.rs",
    "content": "//! Implementation of the abs() builtin function.\n\nuse num_bigint::BigInt;\nuse num_traits::Signed;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{LongInt, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the abs() builtin function.\n///\n/// Returns the absolute value of a number. Works with integers, floats, and LongInts.\n/// For `i64::MIN`, which overflows on negation, promotes to LongInt.\npub fn builtin_abs(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"abs\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    match value {\n        Value::Int(n) => {\n            // Handle potential overflow for i64::MIN → promote to LongInt\n            if let Some(abs_val) = n.checked_abs() {\n                Ok(Value::Int(abs_val))\n            } else {\n                // i64::MIN.abs() overflows, promote to LongInt\n                let bi = BigInt::from(*n).abs();\n                Ok(LongInt::new(bi).into_value(vm.heap)?)\n            }\n        }\n        Value::Float(f) => Ok(Value::Float(f.abs())),\n        Value::Bool(b) => Ok(Value::Int(i64::from(*b))),\n        Value::Ref(id) => {\n            if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                Ok(li.abs().into_value(vm.heap)?)\n            } else {\n                Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"bad operand type for abs(): '{}'\", value.py_type(vm.heap)),\n                )\n                .into())\n            }\n        }\n        _ => Err(SimpleException::new_msg(\n            ExcType::TypeError,\n            format!(\"bad operand type for abs(): '{}'\", value.py_type(vm.heap)),\n        )\n        .into()),\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/all.rs",
    "content": "//! Implementation of the all() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::RunResult,\n    resource::ResourceTracker,\n    types::{MontyIter, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the all() builtin function.\n///\n/// Returns True if all elements of the iterable are true (or if the iterable is empty).\n/// Short-circuits on the first falsy value.\npub fn builtin_all(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let iterable = args.get_one_arg(\"all\", vm.heap)?;\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n\n    while let Some(item) = iter.for_next(vm)? {\n        defer_drop!(item, vm);\n        if !item.py_bool(vm) {\n            return Ok(Value::Bool(false));\n        }\n    }\n\n    Ok(Value::Bool(true))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/any.rs",
    "content": "//! Implementation of the any() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::RunResult,\n    resource::ResourceTracker,\n    types::{MontyIter, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the any() builtin function.\n///\n/// Returns True if any element of the iterable is true.\n/// Returns False for an empty iterable. Short-circuits on the first truthy value.\npub fn builtin_any(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let iterable = args.get_one_arg(\"any\", vm.heap)?;\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n\n    while let Some(item) = iter.for_next(vm)? {\n        defer_drop!(item, vm);\n        if item.py_bool(vm) {\n            return Ok(Value::Bool(true));\n        }\n    }\n\n    Ok(Value::Bool(false))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/bin.rs",
    "content": "//! Implementation of the bin() builtin function.\n\nuse num_bigint::BigInt;\nuse num_traits::Signed;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{PyTrait, Str},\n    value::Value,\n};\n\n/// Implementation of the bin() builtin function.\n///\n/// Converts an integer to a binary string prefixed with '0b'.\n/// Supports both i64 and BigInt integers.\npub fn builtin_bin(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"bin\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    match value {\n        Value::Int(n) => {\n            let abs_digits = format!(\"{:b}\", n.unsigned_abs());\n            let prefix = if *n < 0 { \"-0b\" } else { \"0b\" };\n            let heap_id = vm\n                .heap\n                .allocate(HeapData::Str(Str::new(format!(\"{prefix}{abs_digits}\"))))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Value::Bool(b) => {\n            let s = if *b { \"0b1\" } else { \"0b0\" };\n            let heap_id = vm.heap.allocate(HeapData::Str(Str::new(s.to_string())))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Value::Ref(id) => {\n            if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                let bin_str = format_bigint_bin(li.inner());\n                let heap_id = vm.heap.allocate(HeapData::Str(Str::new(bin_str)))?;\n                Ok(Value::Ref(heap_id))\n            } else {\n                Err(ExcType::type_error_not_integer(value.py_type(vm.heap)))\n            }\n        }\n        _ => Err(ExcType::type_error_not_integer(value.py_type(vm.heap))),\n    }\n}\n\n/// Formats a BigInt as a binary string with '0b' prefix.\nfn format_bigint_bin(bi: &BigInt) -> String {\n    let is_negative = bi.is_negative();\n    let abs_bi = bi.abs();\n    let bin_digits = format!(\"{abs_bi:b}\");\n    let prefix = if is_negative { \"-0b\" } else { \"0b\" };\n    format!(\"{prefix}{bin_digits}\")\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/chr.rs",
    "content": "//! Implementation of the chr() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    resource::ResourceTracker,\n    types::{PyTrait, str::allocate_char},\n    value::Value,\n};\n\n/// Implementation of the chr() builtin function.\n///\n/// Returns a string representing a character whose Unicode code point is the integer.\n/// The valid range for the argument is from 0 through 1,114,111 (0x10FFFF).\npub fn builtin_chr(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"chr\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    match value {\n        Value::Int(n) => {\n            if *n < 0 || *n > 0x0010_FFFF {\n                Err(SimpleException::new_msg(ExcType::ValueError, \"chr() arg not in range(0x110000)\").into())\n            } else if let Some(c) = char::from_u32(u32::try_from(*n).expect(\"chr() range check failed\")) {\n                Ok(allocate_char(c, vm.heap)?)\n            } else {\n                // This shouldn't happen for valid Unicode range, but handle it\n                Err(SimpleException::new_msg(ExcType::ValueError, \"chr() arg not in range(0x110000)\").into())\n            }\n        }\n        Value::Bool(b) => {\n            // bool is subclass of int\n            let c = if *b { '\\x01' } else { '\\x00' };\n            Ok(allocate_char(c, vm.heap)?)\n        }\n        _ => {\n            let type_name = value.py_type(vm.heap);\n            Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"an integer is required (got type {type_name})\"),\n            )\n            .into())\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/divmod.rs",
    "content": "//! Implementation of the divmod() builtin function.\n\nuse num_bigint::BigInt;\nuse num_integer::Integer;\nuse smallvec::smallvec;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::HeapData,\n    resource::{ResourceTracker, check_div_size},\n    types::{LongInt, PyTrait, allocate_tuple},\n    value::{Value, floor_divmod},\n};\n\n/// Implementation of the divmod() builtin function.\n///\n/// Returns a tuple (quotient, remainder) from integer division.\n/// Equivalent to (a // b, a % b).\npub fn builtin_divmod(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (a, b) = args.get_two_args(\"divmod\", vm.heap)?;\n    let a = super::round::normalize_bool_to_int(a);\n    let b = super::round::normalize_bool_to_int(b);\n    defer_drop!(a, vm);\n    defer_drop!(b, vm);\n    let heap = &mut *vm.heap;\n\n    match (a, b) {\n        (Value::Int(x), Value::Int(y)) => {\n            if *y == 0 {\n                Err(ExcType::divmod_by_zero())\n            } else if let Some((quot, rem)) = floor_divmod(*x, *y) {\n                Ok(allocate_tuple(smallvec![Value::Int(quot), Value::Int(rem)], heap)?)\n            } else {\n                // Overflow - promote to BigInt\n                check_div_size(64, heap.tracker())?;\n                let (quot, rem) = bigint_floor_divmod(&BigInt::from(*x), &BigInt::from(*y));\n                let quot_val = LongInt::new(quot).into_value(heap)?;\n                let rem_val = LongInt::new(rem).into_value(heap)?;\n                Ok(allocate_tuple(smallvec![quot_val, rem_val], heap)?)\n            }\n        }\n        (Value::Int(x), Value::Ref(id)) => {\n            if let HeapData::LongInt(li) = heap.get(*id) {\n                if li.is_zero() {\n                    Err(ExcType::divmod_by_zero())\n                } else {\n                    let x_bi = BigInt::from(*x);\n                    let (quot, rem) = bigint_floor_divmod(&x_bi, li.inner());\n                    let quot_val = LongInt::new(quot).into_value(heap)?;\n                    let rem_val = LongInt::new(rem).into_value(heap)?;\n                    Ok(allocate_tuple(smallvec![quot_val, rem_val], heap)?)\n                }\n            } else {\n                let a_type = a.py_type(heap);\n                let b_type = b.py_type(heap);\n                Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"unsupported operand type(s) for divmod(): '{a_type}' and '{b_type}'\"),\n                )\n                .into())\n            }\n        }\n        (Value::Ref(id), Value::Int(y)) => {\n            if let HeapData::LongInt(li) = heap.get(*id) {\n                if *y == 0 {\n                    Err(ExcType::divmod_by_zero())\n                } else {\n                    let y_bi = BigInt::from(*y);\n                    let (quot, rem) = bigint_floor_divmod(li.inner(), &y_bi);\n                    let quot_val = LongInt::new(quot).into_value(heap)?;\n                    let rem_val = LongInt::new(rem).into_value(heap)?;\n                    Ok(allocate_tuple(smallvec![quot_val, rem_val], heap)?)\n                }\n            } else {\n                let a_type = a.py_type(heap);\n                let b_type = b.py_type(heap);\n                Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"unsupported operand type(s) for divmod(): '{a_type}' and '{b_type}'\"),\n                )\n                .into())\n            }\n        }\n        (Value::Ref(id1), Value::Ref(id2)) => {\n            let x_bi = if let HeapData::LongInt(li) = heap.get(*id1) {\n                li.inner().clone()\n            } else {\n                let a_type = a.py_type(heap);\n                let b_type = b.py_type(heap);\n                return Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"unsupported operand type(s) for divmod(): '{a_type}' and '{b_type}'\"),\n                )\n                .into());\n            };\n            if let HeapData::LongInt(li) = heap.get(*id2) {\n                if li.is_zero() {\n                    Err(ExcType::divmod_by_zero())\n                } else {\n                    let (quot, rem) = bigint_floor_divmod(&x_bi, li.inner());\n                    let quot_val = LongInt::new(quot).into_value(heap)?;\n                    let rem_val = LongInt::new(rem).into_value(heap)?;\n                    Ok(allocate_tuple(smallvec![quot_val, rem_val], heap)?)\n                }\n            } else {\n                let a_type = a.py_type(heap);\n                let b_type = b.py_type(heap);\n                Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"unsupported operand type(s) for divmod(): '{a_type}' and '{b_type}'\"),\n                )\n                .into())\n            }\n        }\n        (Value::Float(x), Value::Float(y)) => {\n            if *y == 0.0 {\n                Err(ExcType::divmod_by_zero())\n            } else {\n                let quot = (x / y).floor();\n                let rem = x - quot * y;\n                Ok(allocate_tuple(smallvec![Value::Float(quot), Value::Float(rem)], heap)?)\n            }\n        }\n        (Value::Int(x), Value::Float(y)) => {\n            if *y == 0.0 {\n                Err(ExcType::divmod_by_zero())\n            } else {\n                let xf = *x as f64;\n                let quot = (xf / y).floor();\n                let rem = xf - quot * y;\n                Ok(allocate_tuple(smallvec![Value::Float(quot), Value::Float(rem)], heap)?)\n            }\n        }\n        (Value::Float(x), Value::Int(y)) => {\n            if *y == 0 {\n                Err(ExcType::divmod_by_zero())\n            } else {\n                let yf = *y as f64;\n                let quot = (x / yf).floor();\n                let rem = x - quot * yf;\n                Ok(allocate_tuple(smallvec![Value::Float(quot), Value::Float(rem)], heap)?)\n            }\n        }\n        _ => {\n            let a_type = a.py_type(heap);\n            let b_type = b.py_type(heap);\n            Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"unsupported operand type(s) for divmod(): '{a_type}' and '{b_type}'\"),\n            )\n            .into())\n        }\n    }\n}\n\n/// Computes Python-style floor division and modulo for BigInts.\n///\n/// Uses `div_mod_floor` from num_integer for correct floor semantics.\nfn bigint_floor_divmod(a: &BigInt, b: &BigInt) -> (BigInt, BigInt) {\n    a.div_mod_floor(b)\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/enumerate.rs",
    "content": "//! Implementation of the enumerate() builtin function.\n\nuse smallvec::smallvec;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{List, MontyIter, PyTrait, allocate_tuple},\n    value::Value,\n};\n\n/// Implementation of the enumerate() builtin function.\n///\n/// Returns a list of (index, value) tuples.\n/// Note: In Python this returns an iterator, but we return a list for simplicity.\npub fn builtin_enumerate(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (iterable, start) = args.get_one_two_args(\"enumerate\", vm.heap)?;\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n    defer_drop!(start, vm);\n\n    // Get start index (default 0)\n    let mut index: i64 = match start {\n        Some(Value::Int(n)) => *n,\n        Some(Value::Bool(b)) => i64::from(*b),\n        Some(v) => {\n            let type_name = v.py_type(vm.heap);\n            return Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"'{type_name}' object cannot be interpreted as an integer\"),\n            )\n            .into());\n        }\n        None => 0,\n    };\n\n    let mut result: Vec<Value> = Vec::new();\n\n    while let Some(item) = iter.for_next(vm)? {\n        // Create tuple (index, item)\n        let tuple_val = allocate_tuple(smallvec![Value::Int(index), item], vm.heap)?;\n        result.push(tuple_val);\n        index += 1;\n    }\n\n    let heap_id = vm.heap.allocate(HeapData::List(List::new(result)))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/filter.rs",
    "content": "//! Implementation of the filter() builtin function.\n//!\n//! This module provides the filter() builtin which filters elements from an iterable\n//! based on a predicate function. The implementation supports:\n//! - `None` as predicate (filters falsy values)\n//! - Builtin functions (len, abs, etc.)\n//! - Type constructors (int, str, float, etc.)\n//! - User-defined functions (via `vm.evaluate_function`)\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::RunResult,\n    heap::{HeapData, HeapGuard},\n    resource::ResourceTracker,\n    types::{List, MontyIter, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the filter() builtin function.\n///\n/// Filters elements from an iterable based on a predicate function.\n/// If the predicate is None, filters out falsy values.\n///\n/// Note: In Python this returns an iterator, but we return a list for simplicity.\n///\n/// Examples:\n/// ```python\n/// filter(lambda x: x > 0, [-1, 0, 1, 2])  # [1, 2]\n/// filter(None, [0, 1, False, True, ''])   # [1, True]\n/// ```\npub fn builtin_filter(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (function, iterable) = args.get_two_args(\"filter\", vm.heap)?;\n    defer_drop!(function, vm);\n\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n\n    let out: Vec<Value> = Vec::new();\n    let mut out_guard = HeapGuard::new(out, vm);\n    let (out, vm) = out_guard.as_parts_mut();\n\n    while let Some(item) = iter.for_next(vm)? {\n        let mut item_guard = HeapGuard::new(item, vm);\n        let (item, vm) = item_guard.as_parts_mut();\n        let should_include = if let Value::None = function {\n            // No predicate - use truthiness of element\n            item.py_bool(vm)\n        } else {\n            // Clone for predicate call - the clone is consumed by evaluate_function\n            let item_for_predicate = item.clone_with_heap(vm);\n            let result = vm.evaluate_function(\"filter()\", function, ArgValues::One(item_for_predicate))?;\n            let is_truthy = result.py_bool(vm);\n            result.drop_with_heap(vm);\n            is_truthy\n        };\n\n        if should_include {\n            out.push(item_guard.into_inner());\n        }\n    }\n\n    let (out, vm) = out_guard.into_parts();\n    let heap_id = vm.heap.allocate(HeapData::List(List::new(out)))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/getattr.rs",
    "content": "//! Implementation of the getattr() builtin function.\n\nuse crate::{\n    ExcType,\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{RunResult, SimpleException},\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\n/// Implementation of the getattr() builtin function.\n///\n/// Returns the value of the named attribute of an object.\n/// If the attribute doesn't exist and a default is provided, returns the default.\n/// If no default is provided and the attribute doesn't exist, raises AttributeError.\n///\n/// Note: name must be a string. Per Python docs, \"Since private name mangling happens\n/// at compilation time, one must manually mangle a private attribute's (attributes with\n/// two leading underscores) name in order to retrieve it with getattr().\"\n///\n/// Examples:\n/// ```python\n/// getattr(obj, 'x')             # Get obj.x\n/// getattr(obj, 'y', None)       # Get obj.y or None if not found\n/// getattr(module, 'function')   # Get module.function\n/// ```\npub fn builtin_getattr(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let positional = args.into_pos_only(\"getattr\", vm.heap)?;\n    defer_drop!(positional, vm);\n\n    let (object, name, default) = match positional.as_slice() {\n        too_few @ ([] | [_]) => return Err(ExcType::type_error_at_least(\"getattr\", 2, too_few.len())),\n        [object, name] => (object, name, None),\n        [object, name, default] => (object, name, Some(default)),\n        too_many => return Err(ExcType::type_error_at_most(\"getattr\", 3, too_many.len())),\n    };\n\n    let Some(attr) = name.as_either_str(vm.heap) else {\n        let ty = name.py_type(vm.heap);\n        return Err(\n            SimpleException::new_msg(ExcType::TypeError, format!(\"attribute name must be string, not '{ty}'\")).into(),\n        );\n    };\n\n    match object.py_getattr(&attr, vm) {\n        Ok(CallResult::Value(value)) => Ok(value),\n        Ok(_) => {\n            // getattr() only retrieves attribute values — OS calls, external calls,\n            // method calls, and awaits are not supported here\n            //\n            // TODO: might need to support this case?\n            Err(SimpleException::new_msg(ExcType::TypeError, \"getattr(): attribute is not a simple value\").into())\n        }\n        Err(e) => {\n            if let Some(d) = default {\n                Ok(d.clone_with_heap(vm))\n            } else {\n                Err(e)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/hash.rs",
    "content": "//! Implementation of the hash() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\n/// Implementation of the hash() builtin function.\n///\n/// Returns the hash value of an object (if it has one).\n/// Raises TypeError for unhashable types like lists and dicts.\npub fn builtin_hash(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"hash\", vm.heap)?;\n    defer_drop!(value, vm);\n    match value.py_hash(vm.heap, vm.interns)? {\n        Some(hash) => {\n            // Python's hash() returns a signed integer; reinterpret bits for large values\n            let hash_i64 = i64::from_ne_bytes(hash.to_ne_bytes());\n            Ok(Value::Int(hash_i64))\n        }\n        None => Err(ExcType::type_error_unhashable(value.py_type(vm.heap))),\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/hex.rs",
    "content": "//! Implementation of the hex() builtin function.\n\nuse num_bigint::BigInt;\nuse num_traits::Signed;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{PyTrait, Str},\n    value::Value,\n};\n\n/// Implementation of the hex() builtin function.\n///\n/// Converts an integer to a lowercase hexadecimal string prefixed with '0x'.\n/// Supports both i64 and BigInt integers.\npub fn builtin_hex(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"hex\", vm.heap)?;\n    defer_drop!(value, vm);\n    let heap = &mut *vm.heap;\n\n    match value {\n        Value::Int(n) => {\n            let abs_digits = format!(\"{:x}\", n.unsigned_abs());\n            let prefix = if *n < 0 { \"-0x\" } else { \"0x\" };\n            let heap_id = heap.allocate(HeapData::Str(Str::new(format!(\"{prefix}{abs_digits}\"))))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Value::Bool(b) => {\n            let s = if *b { \"0x1\" } else { \"0x0\" };\n            let heap_id = heap.allocate(HeapData::Str(Str::new(s.to_string())))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Value::Ref(id) => {\n            if let HeapData::LongInt(li) = heap.get(*id) {\n                let hex_str = format_bigint_hex(li.inner());\n                let heap_id = heap.allocate(HeapData::Str(Str::new(hex_str)))?;\n                Ok(Value::Ref(heap_id))\n            } else {\n                Err(ExcType::type_error_not_integer(value.py_type(heap)))\n            }\n        }\n        _ => Err(ExcType::type_error_not_integer(value.py_type(heap))),\n    }\n}\n\n/// Formats a BigInt as a hexadecimal string with '0x' prefix.\nfn format_bigint_hex(bi: &BigInt) -> String {\n    let is_negative = bi.is_negative();\n    let abs_bi = bi.abs();\n    let hex_digits = format!(\"{abs_bi:x}\");\n    let prefix = if is_negative { \"-0x\" } else { \"0x\" };\n    format!(\"{prefix}{hex_digits}\")\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/id.rs",
    "content": "//! Implementation of the id() builtin function.\n\nuse crate::{\n    args::ArgValues, bytecode::VM, defer_drop, exception_private::RunResult, resource::ResourceTracker, value::Value,\n};\n\n/// Implementation of the id() builtin function.\n///\n/// Returns the identity of an object (unique integer for the object's lifetime).\npub fn builtin_id(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"id\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    let id = value.id();\n\n    // Python's id() returns a signed integer; reinterpret bits for large values\n    // On 64-bit: large addresses wrap to negative; on 32-bit: always fits positive\n    #[expect(\n        clippy::cast_possible_wrap,\n        reason = \"Python id() returns signed; wrapping intentional\"\n    )]\n    let id_i64 = id as i64;\n    Ok(Value::Int(id_i64))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/isinstance.rs",
    "content": "//! Implementation of the isinstance() builtin function.\n\nuse super::Builtins;\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapData},\n    resource::ResourceTracker,\n    types::{PyTrait, Type},\n    value::Value,\n};\n\n/// Implementation of the isinstance() builtin function.\n///\n/// Checks if an object is an instance of a class or a tuple of classes.\npub fn builtin_isinstance(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (obj, classinfo) = args.get_two_args(\"isinstance\", vm.heap)?;\n    defer_drop!(obj, vm);\n    defer_drop!(classinfo, vm);\n    let heap = &mut *vm.heap;\n\n    let obj_type = obj.py_type(heap);\n\n    match isinstance_check(obj_type, classinfo, heap) {\n        Ok(result) => Ok(Value::Bool(result)),\n        Err(()) => Err(ExcType::isinstance_arg2_error()),\n    }\n}\n\n/// Recursively checks if obj_type matches classinfo for isinstance().\n///\n/// Returns `Ok(true)` if the type matches, `Ok(false)` if it doesn't,\n/// or `Err(())` if classinfo is invalid (not a type or tuple of types).\n///\n/// Supports:\n/// - Single types: `isinstance(x, int)`\n/// - Exception types: `isinstance(err, ValueError)`\n/// - Exception hierarchy: `isinstance(err, LookupError)` for KeyError/IndexError\n/// - Nested tuples: `isinstance(x, (int, (str, bytes)))`\nfn isinstance_check(obj_type: Type, classinfo: &Value, heap: &Heap<impl ResourceTracker>) -> Result<bool, ()> {\n    match classinfo {\n        // Single type: isinstance(x, int)\n        Value::Builtin(Builtins::Type(t)) => Ok(obj_type.is_instance_of(*t)),\n\n        // Exception type: isinstance(err, ValueError) or isinstance(err, LookupError)\n        Value::Builtin(Builtins::ExcType(handler_type)) => {\n            // Check exception hierarchy using is_subclass_of\n            Ok(matches!(obj_type, Type::Exception(exc_type) if exc_type.is_subclass_of(*handler_type)))\n        }\n\n        // Tuple of types (possibly nested): isinstance(x, (int, (str, bytes)))\n        Value::Ref(id) => {\n            if let HeapData::Tuple(tuple) = heap.get(*id) {\n                for v in tuple.as_slice() {\n                    if isinstance_check(obj_type, v, heap)? {\n                        return Ok(true);\n                    }\n                }\n                Ok(false)\n            } else {\n                Err(()) // Not a tuple - invalid\n            }\n        }\n        _ => Err(()), // Invalid classinfo\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/len.rs",
    "content": "//! Implementation of the len() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\n/// Implementation of the len() builtin function.\n///\n/// Returns the length of an object (number of items in a container).\npub fn builtin_len(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"len\", vm.heap)?;\n    defer_drop!(value, vm);\n    if let Some(len) = value.py_len(vm) {\n        Ok(Value::Int(i64::try_from(len).expect(\"len exceeds i64::MAX\")))\n    } else {\n        let type_name = value.py_type(vm.heap);\n        Err(SimpleException::new_msg(ExcType::TypeError, format!(\"object of type '{type_name}' has no len()\")).into())\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/map.rs",
    "content": "//! Implementation of the map() builtin function.\n\nuse crate::{\n    args::{ArgValues, KwargsValues},\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::{DropWithHeap, HeapData},\n    resource::ResourceTracker,\n    types::{List, MontyIter},\n    value::Value,\n};\n\n/// Implementation of the map() builtin function.\n///\n/// Applies a function to every item of one or more iterables and returns a list of results.\n/// With multiple iterables, stops when the shortest iterable is exhausted.\n///\n/// Note: In Python this returns an iterator, but we return a list for simplicity.\n/// Note: The `strict=` parameter is not yet supported.\n///\n/// Examples:\n/// ```python\n/// map(abs, [-1, 0, 1, 2])           # [1, 0, 1, 2]\n/// map(pow, [2, 3], [3, 2])          # [8, 9]\n/// map(str, [1, 2, 3])               # ['1', '2', '3']\n/// ```\npub fn builtin_map(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (positional, kwargs) = args.into_parts();\n    defer_drop_mut!(positional, vm);\n\n    kwargs.not_supported_yet(\"map\", vm.heap)?;\n\n    if positional.len() < 2 {\n        return Err(SimpleException::new_msg(ExcType::TypeError, \"map() must have at least two arguments.\").into());\n    }\n\n    let function = positional.next().unwrap();\n    defer_drop!(function, vm);\n\n    let first_iterable = positional.next().expect(\"checked length above\");\n    let first_iter = MontyIter::new(first_iterable, vm)?;\n    defer_drop_mut!(first_iter, vm);\n\n    let extra_iterators: Vec<MontyIter> = Vec::with_capacity(positional.len());\n    defer_drop_mut!(extra_iterators, vm);\n\n    for iterable in positional {\n        extra_iterators.push(MontyIter::new(iterable, vm)?);\n    }\n\n    let mut out = Vec::with_capacity(first_iter.size_hint(vm.heap));\n\n    // map function over iterables until the shortest iter is exhausted\n    match extra_iterators.as_mut_slice() {\n        // map(f, iter)\n        [] => {\n            while let Some(item) = first_iter.for_next(vm)? {\n                let args = ArgValues::One(item);\n                out.push(vm.evaluate_function(\"map()\", function, args)?);\n            }\n        }\n        // map(f, iter1, iter2)\n        [single] => {\n            while let Some(arg1) = first_iter.for_next(vm)? {\n                let Some(arg2) = single.for_next(vm)? else {\n                    arg1.drop_with_heap(vm);\n                    break;\n                };\n                let args = ArgValues::Two(arg1, arg2);\n                out.push(vm.evaluate_function(\"map()\", function, args)?);\n            }\n        }\n        // map(f, iter1, iter2, *iterables)\n        multiple => 'outer: loop {\n            let mut items = Vec::with_capacity(1 + multiple.len());\n\n            for iter in std::iter::once(&mut *first_iter).chain(multiple.iter_mut()) {\n                if let Some(item) = iter.for_next(vm)? {\n                    items.push(item);\n                } else {\n                    items.drop_with_heap(vm);\n                    break 'outer;\n                }\n            }\n\n            let args = ArgValues::ArgsKargs {\n                args: items,\n                kwargs: KwargsValues::Empty,\n            };\n\n            out.push(vm.evaluate_function(\"map()\", function, args)?);\n        },\n    }\n\n    let heap_id = vm.heap.allocate(HeapData::List(List::new(out)))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/min_max.rs",
    "content": "//! Implementation of the min() and max() builtin functions.\n\nuse std::cmp::Ordering;\n\nuse crate::{\n    args::{ArgValues, KwargsValues},\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunError, RunResult, SimpleException},\n    heap::{Heap, HeapGuard},\n    heap_traits::DropWithHeap,\n    resource::ResourceTracker,\n    types::{MontyIter, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the min() builtin function.\n///\n/// Returns the smallest item in an iterable or the smallest of two or more arguments.\n/// Supports two forms:\n/// - `min(iterable)` - returns smallest item from iterable\n/// - `min(arg1, arg2, ...)` - returns smallest of the arguments\npub fn builtin_min(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    builtin_min_max(vm, args, true)\n}\n\n/// Implementation of the max() builtin function.\n///\n/// Returns the largest item in an iterable or the largest of two or more arguments.\n/// Supports two forms:\n/// - `max(iterable)` - returns largest item from iterable\n/// - `max(arg1, arg2, ...)` - returns largest of the arguments\npub fn builtin_max(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    builtin_min_max(vm, args, false)\n}\n\n/// Shared implementation for min() and max().\n///\n/// When `is_min` is true, returns the minimum; otherwise returns the maximum.\nfn builtin_min_max(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues, is_min: bool) -> RunResult<Value> {\n    let func_name = if is_min { \"min\" } else { \"max\" };\n    let key_context = if is_min {\n        \"min() key argument\"\n    } else {\n        \"max() key argument\"\n    };\n    let (positional, kwargs) = args.into_parts();\n    defer_drop_mut!(positional, vm);\n\n    let Some(first_arg) = positional.next() else {\n        kwargs.drop_with_heap(vm);\n        return Err(SimpleException::new_msg(\n            ExcType::TypeError,\n            format!(\"{func_name} expected at least 1 argument, got 0\"),\n        )\n        .into());\n    };\n\n    let mut first_arg_guard = HeapGuard::new(first_arg, vm);\n    let (key_fn, default_value) = parse_min_max_kwargs(kwargs, func_name, first_arg_guard.heap())?;\n    let (first_arg, vm) = first_arg_guard.into_parts();\n    defer_drop!(key_fn, vm);\n    let mut default_guard = HeapGuard::new(default_value, vm);\n    let (default_value, vm) = default_guard.as_parts_mut();\n\n    // decide what to do based on remaining arguments\n    if positional.len() == 0 {\n        // Single argument: iterate over it\n        let iter = MontyIter::new(first_arg, vm)?;\n        defer_drop_mut!(iter, vm);\n\n        let Some(result) = iter.for_next(vm)? else {\n            if let Some(default) = default_value.take() {\n                return Ok(default);\n            }\n            return Err(SimpleException::new_msg(\n                ExcType::ValueError,\n                format!(\"{func_name}() iterable argument is empty\"),\n            )\n            .into());\n        };\n\n        if let Some(key_fn) = key_fn {\n            let mut result_guard = HeapGuard::new(result, vm);\n            {\n                let (result, vm) = result_guard.as_parts_mut();\n                let result_key = evaluate_key(result.clone_with_heap(vm), key_fn, key_context, vm)?;\n                let mut result_key_guard = HeapGuard::new(result_key, vm);\n                {\n                    let (result_key, vm) = result_key_guard.as_parts_mut();\n\n                    while let Some(item) = iter.for_next(vm)? {\n                        defer_drop_mut!(item, vm);\n                        let item_key = evaluate_key(item.clone_with_heap(vm), key_fn, key_context, vm)?;\n                        defer_drop_mut!(item_key, vm);\n\n                        if candidate_wins(result_key, item_key, is_min, vm)? {\n                            std::mem::swap(result, item);\n                            std::mem::swap(result_key, item_key);\n                        }\n                    }\n                }\n\n                let result_key = result_key_guard.into_inner();\n                result_key.drop_with_heap(vm);\n            }\n            Ok(result_guard.into_inner())\n        } else {\n            let mut result_guard = HeapGuard::new(result, vm);\n            let (result, vm) = result_guard.as_parts_mut();\n\n            while let Some(item) = iter.for_next(vm)? {\n                defer_drop_mut!(item, vm);\n\n                if candidate_wins(result, item, is_min, vm)? {\n                    std::mem::swap(result, item);\n                }\n            }\n\n            Ok(result_guard.into_inner())\n        }\n    } else {\n        // Multiple arguments: compare them directly\n        if default_value.is_some() {\n            first_arg.drop_with_heap(vm);\n            return Err(default_with_multiple_args(func_name));\n        }\n\n        if let Some(key_fn) = key_fn {\n            let mut result_guard = HeapGuard::new(first_arg, vm);\n            {\n                let (result, vm) = result_guard.as_parts_mut();\n                let result_key = evaluate_key(result.clone_with_heap(vm), key_fn, key_context, vm)?;\n                let mut result_key_guard = HeapGuard::new(result_key, vm);\n                {\n                    let (result_key, vm) = result_key_guard.as_parts_mut();\n\n                    for item in positional {\n                        defer_drop_mut!(item, vm);\n                        let item_key = evaluate_key(item.clone_with_heap(vm), key_fn, key_context, vm)?;\n                        defer_drop_mut!(item_key, vm);\n\n                        if candidate_wins(result_key, item_key, is_min, vm)? {\n                            std::mem::swap(result, item);\n                            std::mem::swap(result_key, item_key);\n                        }\n                    }\n                }\n\n                let result_key = result_key_guard.into_inner();\n                result_key.drop_with_heap(vm);\n            }\n            Ok(result_guard.into_inner())\n        } else {\n            let mut result_guard = HeapGuard::new(first_arg, vm);\n            let (result, vm) = result_guard.as_parts_mut();\n\n            for item in positional {\n                defer_drop_mut!(item, vm);\n\n                if candidate_wins(result, item, is_min, vm)? {\n                    std::mem::swap(result, item);\n                }\n            }\n\n            Ok(result_guard.into_inner())\n        }\n    }\n}\n\n/// Parses `key=` and `default=` for min()/max().\n///\n/// Returns `(key_fn, default_value)`. Passing `key=None` is normalized to `None`\n/// so the comparison logic can treat it the same as omitting the keyword.\nfn parse_min_max_kwargs(\n    kwargs: KwargsValues,\n    func_name: &str,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Option<Value>, Option<Value>)> {\n    let (key_fn, default_value) = kwargs.parse_named_kwargs_pair(\n        func_name,\n        \"key\",\n        \"default\",\n        vm.heap,\n        vm.interns,\n        ExcType::type_error_unexpected_keyword,\n    )?;\n\n    let key_fn = match key_fn {\n        Some(value) if matches!(value, Value::None) => {\n            value.drop_with_heap(vm);\n            None\n        }\n        other => other,\n    };\n\n    Ok((key_fn, default_value))\n}\n\n/// Calls the user-provided key function for a single candidate value.\n///\n/// The caller passes an owned clone of the candidate so this helper can forward it\n/// into the function call without changing ownership of the original item being\n/// tracked as the eventual min/max result.\nfn evaluate_key(\n    item: Value,\n    key_fn: &Value,\n    key_context: &'static str,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Value> {\n    vm.evaluate_function(key_context, key_fn, ArgValues::One(item))\n}\n\n/// Returns whether `candidate` should replace `current` as the best value seen so far.\n///\n/// `min()` replaces the current winner when the new candidate compares smaller,\n/// while `max()` replaces it when the new candidate compares larger. Equal values\n/// keep the existing winner so ties preserve the first-seen item, matching CPython.\nfn candidate_wins(\n    current: &Value,\n    candidate: &Value,\n    is_min: bool,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<bool> {\n    let Some(ordering) = candidate.py_cmp(current, vm)? else {\n        return Err(ord_not_supported(candidate, current, is_min, vm.heap));\n    };\n\n    Ok((is_min && ordering == Ordering::Less) || (!is_min && ordering == Ordering::Greater))\n}\n\n/// Creates the CPython-compatible error for `default=` with multiple positional args.\n#[cold]\nfn default_with_multiple_args(func_name: &str) -> RunError {\n    SimpleException::new_msg(\n        ExcType::TypeError,\n        format!(\"Cannot specify a default for {func_name}() with multiple positional arguments\"),\n    )\n    .into()\n}\n\n#[cold]\nfn ord_not_supported(left: &Value, right: &Value, is_min: bool, heap: &Heap<impl ResourceTracker>) -> RunError {\n    let left_type = left.py_type(heap);\n    let right_type = right.py_type(heap);\n    let operator = if is_min { '<' } else { '>' };\n    ExcType::type_error(format!(\n        \"'{operator}' not supported between instances of '{left_type}' and '{right_type}'\"\n    ))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/mod.rs",
    "content": "//! Python builtin functions, types, and exception constructors.\n//!\n//! This module provides the interpreter-native implementation of Python builtins.\n//! Each builtin function has its own submodule for organization.\n\nmod abs;\nmod all;\nmod any;\nmod bin;\nmod chr;\nmod divmod;\nmod enumerate;\nmod filter;\nmod getattr;\nmod hash;\nmod hex;\nmod id;\nmod isinstance;\nmod len;\nmod map;\nmod min_max; // min and max share implementation\nmod next;\nmod oct;\nmod ord;\nmod pow;\nmod print;\nmod repr;\nmod reversed;\nmod round;\nmod sorted;\nmod sum;\nmod type_;\nmod zip;\n\nuse std::{fmt::Write, str::FromStr};\n\nuse strum::{Display, EnumString, FromRepr, IntoStaticStr};\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    exception_private::{ExcType, RunResult},\n    resource::ResourceTracker,\n    types::Type,\n    value::Value,\n};\n\n/// Enumerates every interpreter-native Python builtins\n///\n/// Uses strum derives for automatic `Display`, `FromStr`, and `AsRef<str>` implementations.\n/// All variants serialize to lowercase (e.g., `Print` -> \"print\").\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) enum Builtins {\n    /// A builtin function like `print`, `len`, `type`, etc.\n    Function(BuiltinsFunctions),\n    /// An exception type constructor like `ValueError`, `TypeError`, etc.\n    ExcType(ExcType),\n    /// A type constructor like `list`, `dict`, `int`, etc.\n    Type(Type),\n}\n\nimpl Builtins {\n    /// Calls this builtin with the given arguments.\n    pub fn call(self, vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        match self {\n            Self::Function(b) => b.call(vm, args),\n            Self::ExcType(exc) => exc.call(vm, args),\n            Self::Type(t) => t.call(vm, args),\n        }\n    }\n\n    /// Writes the Python repr() string for this callable to a formatter.\n    pub fn py_repr_fmt<W: Write>(self, f: &mut W) -> std::fmt::Result {\n        match self {\n            Self::Function(b) => write!(f, \"<built-in function {b}>\"),\n            Self::ExcType(e) => write!(f, \"<class '{e}'>\"),\n            Self::Type(t) => write!(f, \"<class '{t}'>\"),\n        }\n    }\n\n    /// Returns the type of this builtin.\n    pub fn py_type(self) -> Type {\n        match self {\n            Self::Function(_) => Type::BuiltinFunction,\n            Self::ExcType(_) => Type::Type,\n            Self::Type(_) => Type::Type,\n        }\n    }\n}\n\nimpl FromStr for Builtins {\n    type Err = ();\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        // Priority: BuiltinsFunctions > ExcType > Type\n        // Only matches names that are true Python builtins (accessible without imports).\n        if let Ok(b) = BuiltinsFunctions::from_str(s) {\n            Ok(Self::Function(b))\n        } else if let Ok(exc) = ExcType::from_str(s) {\n            Ok(Self::ExcType(exc))\n        } else if let Some(t) = Type::from_builtin_name(s) {\n            Ok(Self::Type(t))\n        } else {\n            Err(())\n        }\n    }\n}\n\n/// Enumerates every interpreter-native Python builtin function.\n///\n/// Listed alphabetically per https://docs.python.org/3/library/functions.html\n/// Commented-out variants are not yet implemented.\n///\n/// Note: Type constructors are handled by the `Type` enum, not here.\n///\n/// Uses strum derives for automatic `Display`, `FromStr`, and `IntoStaticStr` implementations.\n/// All variants serialize to lowercase (e.g., `Print` -> \"print\").\n#[derive(\n    Debug,\n    Clone,\n    Copy,\n    Display,\n    EnumString,\n    FromRepr,\n    IntoStaticStr,\n    PartialEq,\n    Eq,\n    Hash,\n    serde::Serialize,\n    serde::Deserialize,\n)]\n#[strum(serialize_all = \"lowercase\")]\n#[repr(u8)]\npub enum BuiltinsFunctions {\n    Abs,\n    // Aiter,\n    All,\n    // Anext,\n    Any,\n    // Ascii,\n    Bin,\n    // bool - handled by Type enum\n    // Breakpoint,\n    // bytearray - handled by Type enum\n    // bytes - handled by Type enum\n    // Callable,\n    Chr,\n    // Classmethod,\n    // Compile,\n    // complex - handled by Type enum\n    // Delattr,\n    // dict - handled by Type enum\n    // Dir,\n    Divmod,\n    Enumerate,\n    // Eval,\n    // Exec,\n    Filter,\n    // float - handled by Type enum\n    // Format,\n    // frozenset - handled by Type enum\n    Getattr,\n    // Globals,\n    // Hasattr,\n    Hash,\n    // Help,\n    Hex,\n    Id,\n    // Input,\n    // int - handled by Type enum\n    Isinstance,\n    // Issubclass,\n    // Iter - handled by Type enum\n    Len,\n    // list - handled by Type enum\n    // Locals,\n    Map,\n    Max,\n    // memoryview - handled by Type enum\n    Min,\n    Next,\n    // object - handled by Type enum\n    Oct,\n    // Open,\n    Ord,\n    Pow,\n    Print,\n    // Property,\n    // range - handled by Type enum\n    Repr,\n    Reversed,\n    Round,\n    // set - handled by Type enum\n    // Setattr,\n    // Slice,\n    Sorted,\n    // Staticmethod,\n    // str - handled by Type enum\n    Sum,\n    // Super,\n    // tuple - handled by Type enum\n    Type,\n    // Vars,\n    Zip,\n    // __import__ - not planned\n}\n\nimpl BuiltinsFunctions {\n    /// Executes the builtin with the provided arguments.\n    ///\n    /// All builtins receive the full VM context, which provides access to the heap,\n    /// interned strings, and print output.\n    pub(crate) fn call(self, vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        match self {\n            Self::Abs => abs::builtin_abs(vm, args),\n            Self::All => all::builtin_all(vm, args),\n            Self::Any => any::builtin_any(vm, args),\n            Self::Bin => bin::builtin_bin(vm, args),\n            Self::Chr => chr::builtin_chr(vm, args),\n            Self::Divmod => divmod::builtin_divmod(vm, args),\n            Self::Enumerate => enumerate::builtin_enumerate(vm, args),\n            Self::Filter => filter::builtin_filter(vm, args),\n            Self::Getattr => getattr::builtin_getattr(vm, args),\n            Self::Hash => hash::builtin_hash(vm, args),\n            Self::Hex => hex::builtin_hex(vm, args),\n            Self::Id => id::builtin_id(vm, args),\n            Self::Isinstance => isinstance::builtin_isinstance(vm, args),\n            Self::Len => len::builtin_len(vm, args),\n            Self::Map => map::builtin_map(vm, args),\n            Self::Max => min_max::builtin_max(vm, args),\n            Self::Min => min_max::builtin_min(vm, args),\n            Self::Next => next::builtin_next(vm, args),\n            Self::Oct => oct::builtin_oct(vm, args),\n            Self::Ord => ord::builtin_ord(vm, args),\n            Self::Pow => pow::builtin_pow(vm, args),\n            Self::Print => print::builtin_print(vm, args),\n            Self::Repr => repr::builtin_repr(vm, args),\n            Self::Reversed => reversed::builtin_reversed(vm, args),\n            Self::Round => round::builtin_round(vm, args),\n            Self::Sorted => sorted::builtin_sorted(vm, args),\n            Self::Sum => sum::builtin_sum(vm, args),\n            Self::Type => type_::builtin_type(vm, args),\n            Self::Zip => zip::builtin_zip(vm, args),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/next.rs",
    "content": "//! Implementation of the next() builtin function.\n\nuse crate::{\n    args::ArgValues, bytecode::VM, defer_drop, exception_private::RunResult, resource::ResourceTracker,\n    types::iter::iterator_next, value::Value,\n};\n\n/// Implementation of the next() builtin function.\n///\n/// Retrieves the next item from an iterator.\n///\n/// Two forms are supported:\n/// - `next(iterator)` - Returns the next item from the iterator. Raises\n///   `StopIteration` when the iterator is exhausted.\n/// - `next(iterator, default)` - Returns the next item from the iterator, or\n///   `default` if the iterator is exhausted.\npub fn builtin_next(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (iterator, default) = args.get_one_two_args(\"next\", vm.heap)?;\n    defer_drop!(iterator, vm);\n    iterator_next(iterator, default, vm.heap, vm.interns)\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/oct.rs",
    "content": "//! Implementation of the oct() builtin function.\n\nuse num_bigint::BigInt;\nuse num_traits::Signed;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{PyTrait, Str},\n    value::Value,\n};\n\n/// Implementation of the oct() builtin function.\n///\n/// Converts an integer to an octal string prefixed with '0o'.\n/// Supports both i64 and BigInt integers.\npub fn builtin_oct(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"oct\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    match value {\n        Value::Int(n) => {\n            let abs_digits = format!(\"{:o}\", n.unsigned_abs());\n            let prefix = if *n < 0 { \"-0o\" } else { \"0o\" };\n            let heap_id = vm\n                .heap\n                .allocate(HeapData::Str(Str::new(format!(\"{prefix}{abs_digits}\"))))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Value::Bool(b) => {\n            let s = if *b { \"0o1\" } else { \"0o0\" };\n            let heap_id = vm.heap.allocate(HeapData::Str(Str::new(s.to_string())))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Value::Ref(id) => {\n            if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                let oct_str = format_bigint_oct(li.inner());\n                let heap_id = vm.heap.allocate(HeapData::Str(Str::new(oct_str)))?;\n                Ok(Value::Ref(heap_id))\n            } else {\n                Err(ExcType::type_error_not_integer(value.py_type(vm.heap)))\n            }\n        }\n        _ => Err(ExcType::type_error_not_integer(value.py_type(vm.heap))),\n    }\n}\n\n/// Formats a BigInt as an octal string with '0o' prefix.\nfn format_bigint_oct(bi: &BigInt) -> String {\n    let is_negative = bi.is_negative();\n    let abs_bi = bi.abs();\n    let oct_digits = format!(\"{abs_bi:o}\");\n    let prefix = if is_negative { \"-0o\" } else { \"0o\" };\n    format!(\"{prefix}{oct_digits}\")\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/ord.rs",
    "content": "//! Implementation of the ord() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\n/// Implementation of the ord() builtin function.\n///\n/// Returns the Unicode code point of a one-character string.\npub fn builtin_ord(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"ord\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    match value {\n        Value::InternString(string_id) => {\n            let s = vm.interns.get_str(*string_id);\n            let mut chars = s.chars();\n            if let (Some(c), None) = (chars.next(), chars.next()) {\n                Ok(Value::Int(c as i64))\n            } else {\n                let len = s.chars().count();\n                Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"ord() expected a character, but string of length {len} found\"),\n                )\n                .into())\n            }\n        }\n        Value::Ref(id) => {\n            if let HeapData::Str(s) = vm.heap.get(*id) {\n                let mut chars = s.as_str().chars();\n                if let (Some(c), None) = (chars.next(), chars.next()) {\n                    Ok(Value::Int(c as i64))\n                } else {\n                    let len = s.as_str().chars().count();\n                    Err(SimpleException::new_msg(\n                        ExcType::TypeError,\n                        format!(\"ord() expected a character, but string of length {len} found\"),\n                    )\n                    .into())\n                }\n            } else {\n                let type_name = value.py_type(vm.heap);\n                Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    format!(\"ord() expected string of length 1, but {type_name} found\"),\n                )\n                .into())\n            }\n        }\n        _ => {\n            let type_name = value.py_type(vm.heap);\n            Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"ord() expected string of length 1, but {type_name} found\"),\n            )\n            .into())\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/pow.rs",
    "content": "//! Implementation of the pow() builtin function.\n\nuse num_bigint::BigInt;\nuse num_traits::{Signed, ToPrimitive, Zero};\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::{Heap, HeapData},\n    resource::{ResourceTracker, check_pow_size},\n    types::{LongInt, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the pow() builtin function.\n///\n/// Returns base to the power exp. With three arguments, returns (base ** exp) % mod.\n/// Handles negative exponents by returning a float.\npub fn builtin_pow(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    // pow() accepts 2 or 3 arguments\n    let positional = args.into_pos_only(\"pow\", vm.heap)?;\n    defer_drop!(positional, vm);\n\n    match positional.as_slice() {\n        [base, exp] => {\n            let base = normalize_bool(base);\n            let exp = normalize_bool(exp);\n            two_arg_pow(base, exp, vm.heap)\n        }\n        [base, exp, m] => {\n            let base = normalize_bool(base);\n            let exp = normalize_bool(exp);\n            let m = normalize_bool(m);\n            // Three-argument pow: modular exponentiation\n            match (base, exp, m) {\n                (Value::Int(b), Value::Int(e), Value::Int(m_val)) => {\n                    if *m_val == 0 {\n                        Err(SimpleException::new_msg(ExcType::ValueError, \"pow() 3rd argument cannot be 0\").into())\n                    } else if *e < 0 {\n                        Err(SimpleException::new_msg(\n                            ExcType::ValueError,\n                            \"pow() 2nd argument cannot be negative when 3rd argument specified\",\n                        )\n                        .into())\n                    } else {\n                        // Use modular exponentiation\n                        let result = mod_pow(\n                            *b,\n                            u64::try_from(*e).expect(\"pow exponent >= 0 but failed u64 conversion\"),\n                            *m_val,\n                        );\n                        Ok(Value::Int(result))\n                    }\n                }\n                _ => Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    \"pow() 3rd argument not allowed unless all arguments are integers\",\n                )\n                .into()),\n            }\n        }\n        args => Err(SimpleException::new_msg(\n            ExcType::TypeError,\n            format!(\"pow expected 2 or 3 arguments, got {}\", args.len()),\n        )\n        .into()),\n    }\n}\n\n/// Normalizes a `Bool` to its `Int` equivalent by reference.\n///\n/// Returns `&Value::Int(0)` or `&Value::Int(1)` for bools (using static storage),\n/// and the original reference unchanged for all other types.\nfn normalize_bool(value: &Value) -> &Value {\n    static FALSE_INT: Value = Value::Int(0);\n    static TRUE_INT: Value = Value::Int(1);\n    match value {\n        Value::Bool(false) => &FALSE_INT,\n        Value::Bool(true) => &TRUE_INT,\n        other => other,\n    }\n}\n\n/// Computes (base^exp) % modulo using binary exponentiation.\n///\n/// Handles negative bases correctly using Python's modulo semantics.\nfn mod_pow(base: i64, exp: u64, modulo: i64) -> i64 {\n    if modulo == 1 {\n        return 0;\n    }\n\n    let modulo_u = u128::from(modulo.unsigned_abs());\n    let mut result: u128 = 1;\n    let mut b = base.rem_euclid(modulo) as u128;\n    let mut e = exp;\n\n    while e > 0 {\n        if e % 2 == 1 {\n            result = (result * b) % modulo_u;\n        }\n        e /= 2;\n        b = (b * b) % modulo_u;\n    }\n\n    // Convert back to signed, handling negative modulo\n    // result < modulo_u <= i64::MAX as u128, so this conversion is safe\n    let result_i64 = i64::try_from(result).expect(\"mod_pow result exceeds i64::MAX\");\n    if modulo < 0 && result_i64 > 0 {\n        result_i64 + modulo\n    } else {\n        result_i64\n    }\n}\n\nfn checked_pow_i64(mut base: i64, mut exp: u32) -> Option<i64> {\n    let mut result: i64 = 1;\n\n    while exp > 0 {\n        if exp & 1 == 1 {\n            result = result.checked_mul(base)?;\n        }\n        exp >>= 1;\n        if exp > 0 {\n            base = base.checked_mul(base)?;\n        }\n    }\n\n    Some(result)\n}\n\n/// Implements two-argument pow with LongInt support.\n///\n/// On overflow, promotes to LongInt instead of returning an error.\nfn two_arg_pow(base: &Value, exp: &Value, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    match (base, exp) {\n        (Value::Int(b), Value::Int(e)) => int_pow_int(*b, *e, heap),\n        (Value::Int(b), Value::Ref(id)) => {\n            // Clone to avoid borrow conflict with heap mutation\n            let e_bi = if let HeapData::LongInt(li) = heap.get(*id) {\n                li.inner().clone()\n            } else {\n                return Err(ExcType::binary_type_error(\n                    \"** or pow()\",\n                    base.py_type(heap),\n                    exp.py_type(heap),\n                ));\n            };\n            int_pow_longint(*b, &e_bi, heap)\n        }\n        (Value::Ref(id), Value::Int(e)) => {\n            // Clone to avoid borrow conflict with heap mutation\n            let b_bi = if let HeapData::LongInt(li) = heap.get(*id) {\n                li.inner().clone()\n            } else {\n                return Err(ExcType::binary_type_error(\n                    \"** or pow()\",\n                    base.py_type(heap),\n                    exp.py_type(heap),\n                ));\n            };\n            longint_pow_int(&b_bi, *e, heap)\n        }\n        (Value::Ref(id1), Value::Ref(id2)) => {\n            // Clone both to avoid borrow conflict with heap mutation\n            let b_bi = if let HeapData::LongInt(li) = heap.get(*id1) {\n                li.inner().clone()\n            } else {\n                return Err(ExcType::binary_type_error(\n                    \"** or pow()\",\n                    base.py_type(heap),\n                    exp.py_type(heap),\n                ));\n            };\n            let e_bi = if let HeapData::LongInt(li) = heap.get(*id2) {\n                li.inner().clone()\n            } else {\n                return Err(ExcType::binary_type_error(\n                    \"** or pow()\",\n                    base.py_type(heap),\n                    exp.py_type(heap),\n                ));\n            };\n            longint_pow_longint(&b_bi, &e_bi, heap)\n        }\n        (Value::Float(b), Value::Float(e)) => {\n            if *b == 0.0 && *e < 0.0 {\n                Err(ExcType::zero_negative_power())\n            } else {\n                Ok(Value::Float(b.powf(*e)))\n            }\n        }\n        (Value::Int(b), Value::Float(e)) => {\n            if *b == 0 && *e < 0.0 {\n                Err(ExcType::zero_negative_power())\n            } else {\n                Ok(Value::Float((*b as f64).powf(*e)))\n            }\n        }\n        (Value::Float(b), Value::Int(e)) => {\n            if *b == 0.0 && *e < 0 {\n                Err(ExcType::zero_negative_power())\n            } else if let Ok(exp_i32) = i32::try_from(*e) {\n                Ok(Value::Float(b.powi(exp_i32)))\n            } else {\n                Ok(Value::Float(b.powf(*e as f64)))\n            }\n        }\n        _ => Err(ExcType::binary_type_error(\n            \"** or pow()\",\n            base.py_type(heap),\n            exp.py_type(heap),\n        )),\n    }\n}\n\n/// int ** int with LongInt promotion on overflow.\nfn int_pow_int(b: i64, e: i64, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if e < 0 {\n        // Negative exponent returns float\n        if b == 0 {\n            return Err(ExcType::zero_negative_power());\n        }\n        Ok(Value::Float((b as f64).powf(e as f64)))\n    } else if let Ok(exp_u32) = u32::try_from(e) {\n        if let Some(v) = checked_pow_i64(b, exp_u32) {\n            Ok(Value::Int(v))\n        } else {\n            // Overflow - promote to LongInt\n            // Check size before computing to prevent DoS\n            check_pow_size(i64_bits(b), u64::from(exp_u32), heap.tracker())?;\n            let bi = BigInt::from(b).pow(exp_u32);\n            Ok(LongInt::new(bi).into_value(heap)?)\n        }\n    } else {\n        // Exponent too large for u32 - use BigInt for result\n        // Safety: e >= 0 at this point\n        #[expect(clippy::cast_sign_loss)]\n        let exp_u64 = e as u64;\n        // Check size before computing to prevent DoS\n        check_pow_size(i64_bits(b), exp_u64, heap.tracker())?;\n        let base_bi = BigInt::from(b);\n        let bi = bigint_pow_large(&base_bi, exp_u64)?;\n        Ok(LongInt::new(bi).into_value(heap)?)\n    }\n}\n\n/// int ** LongInt with LongInt result.\nfn int_pow_longint(b: i64, e: &BigInt, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if b == 0 && e.is_negative() {\n        return Err(ExcType::zero_negative_power());\n    }\n    if e.is_negative() {\n        // Negative LongInt exponent: return float\n        if let Some(e_f64) = e.to_f64() {\n            Ok(Value::Float((b as f64).powf(e_f64)))\n        } else {\n            Ok(Value::Float(0.0))\n        }\n    } else if e.is_zero() {\n        // x ** 0 = 1 for all x (including 0 ** 0 = 1)\n        Ok(Value::Int(1))\n    } else if b == 0 {\n        Ok(Value::Int(0))\n    } else if b == 1 {\n        Ok(Value::Int(1))\n    } else if b == -1 {\n        // (-1) ** n = 1 if n is even, -1 if n is odd\n        let is_even = (e % 2i32).is_zero();\n        Ok(Value::Int(if is_even { 1 } else { -1 }))\n    } else if let Some(exp_u32) = e.to_u32() {\n        // Check size before computing to prevent DoS\n        check_pow_size(i64_bits(b), u64::from(exp_u32), heap.tracker())?;\n        let bi = BigInt::from(b).pow(exp_u32);\n        Ok(LongInt::new(bi).into_value(heap)?)\n    } else {\n        // Exponent too large\n        Err(ExcType::overflow_exponent_too_large())\n    }\n}\n\n/// LongInt ** int with LongInt result.\nfn longint_pow_int(b: &BigInt, e: i64, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if b.is_zero() && e < 0 {\n        return Err(ExcType::zero_negative_power());\n    }\n    if e < 0 {\n        // Negative exponent: return float\n        if let (Some(b_f64), Some(e_f64)) = (b.to_f64(), Some(e as f64)) {\n            Ok(Value::Float(b_f64.powf(e_f64)))\n        } else {\n            Ok(Value::Float(0.0))\n        }\n    } else if let Ok(exp_u32) = u32::try_from(e) {\n        // Check size before computing to prevent DoS\n        check_pow_size(b.bits(), u64::from(exp_u32), heap.tracker())?;\n        let bi = b.pow(exp_u32);\n        Ok(LongInt::new(bi).into_value(heap)?)\n    } else {\n        // Exponent too large for u32\n        // Safety: e >= 0 at this point\n        #[expect(clippy::cast_sign_loss)]\n        let exp_u64 = e as u64;\n        // Check size before computing to prevent DoS\n        check_pow_size(b.bits(), exp_u64, heap.tracker())?;\n        let bi = bigint_pow_large(b, exp_u64)?;\n        Ok(LongInt::new(bi).into_value(heap)?)\n    }\n}\n\n/// LongInt ** LongInt with LongInt result.\nfn longint_pow_longint(b: &BigInt, e: &BigInt, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if b.is_zero() && e.is_negative() {\n        return Err(ExcType::zero_negative_power());\n    }\n    if e.is_negative() {\n        // Negative exponent: return float\n        if let (Some(b_f64), Some(e_f64)) = (b.to_f64(), e.to_f64()) {\n            Ok(Value::Float(b_f64.powf(e_f64)))\n        } else {\n            Ok(Value::Float(0.0))\n        }\n    } else if let Some(exp_u32) = e.to_u32() {\n        // Check size before computing to prevent DoS\n        check_pow_size(b.bits(), u64::from(exp_u32), heap.tracker())?;\n        let bi = b.pow(exp_u32);\n        Ok(LongInt::new(bi).into_value(heap)?)\n    } else {\n        // Exponent too large\n        Err(ExcType::overflow_exponent_too_large())\n    }\n}\n\n/// BigInt power for large exponents (> u32::MAX).\n///\n/// This handles exponents that are too large for the standard pow function.\n/// For most bases, the result would be astronomically large, so we only handle\n/// special cases (0, 1, -1) and return an error for others.\nfn bigint_pow_large(base: &BigInt, exp: u64) -> RunResult<BigInt> {\n    if base.is_zero() {\n        Ok(BigInt::from(0))\n    } else if *base == BigInt::from(1) {\n        Ok(BigInt::from(1))\n    } else if *base == BigInt::from(-1) {\n        // (-1) ** n = 1 if n is even, -1 if n is odd\n        if exp.is_multiple_of(2) {\n            Ok(BigInt::from(1))\n        } else {\n            Ok(BigInt::from(-1))\n        }\n    } else {\n        // For any other base, exponent > u32::MAX would produce an astronomically large result\n        Err(ExcType::overflow_exponent_too_large())\n    }\n}\n\n/// Computes the number of significant bits in an i64.\nfn i64_bits(value: i64) -> u64 {\n    if value == 0 {\n        0\n    } else {\n        u64::from(64 - value.unsigned_abs().leading_zeros())\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/print.rs",
    "content": "//! Implementation of the print() builtin function.\n\nuse crate::{\n    args::{ArgValues, KwargsValues},\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunError, RunResult, SimpleException},\n    heap::{Heap, HeapData},\n    intern::Interns,\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\n/// Implementation of the print() builtin function.\n///\n/// Supports the following keyword arguments:\n/// - `sep`: separator between values (default: \" \")\n/// - `end`: string appended after the last value (default: \"\\n\")\n/// - `flush`: whether to flush the stream (accepted but ignored)\n///\n/// The `file` kwarg is not supported.\npub fn builtin_print(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    // Split into positional args and kwargs\n    let (positional, kwargs) = args.into_parts();\n    defer_drop!(positional, vm);\n\n    // Extract kwargs first\n    let (sep, end) = extract_print_kwargs(kwargs, vm.heap, vm.interns)?;\n\n    // Print positional args with separator, dropping each value after use\n    let mut first = true;\n    for value in positional.as_slice() {\n        if first {\n            first = false;\n        } else if let Some(sep) = &sep {\n            vm.print_writer.stdout_write(sep.as_str().into())?;\n        } else {\n            vm.print_writer.stdout_push(' ')?;\n        }\n        vm.print_writer.stdout_write(value.py_str(vm))?;\n    }\n\n    // Append end string\n    if let Some(end) = end {\n        vm.print_writer.stdout_write(end.into())?;\n    } else {\n        vm.print_writer.stdout_push('\\n')?;\n    }\n\n    Ok(Value::None)\n}\n\n/// Extracts sep and end kwargs from print() arguments.\n///\n/// Consumes the kwargs, dropping all values after extraction.\n/// Returns (sep, end, error) where error is Some if a kwarg error occurred.\nfn extract_print_kwargs(\n    kwargs: KwargsValues,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<(Option<String>, Option<String>)> {\n    let mut sep: Option<String> = None;\n    let mut end: Option<String> = None;\n    let mut error: Option<RunError> = None;\n\n    for (key, value) in kwargs {\n        // defer_drop! ensures key and value are cleaned up on every path through\n        // the loop body — including continue, early return, and normal iteration\n        defer_drop!(key, heap);\n        defer_drop!(value, heap);\n\n        // If we already hit an error, just drop remaining values\n        if error.is_some() {\n            continue;\n        }\n\n        let Some(keyword_name) = key.as_either_str(heap) else {\n            error = Some(SimpleException::new_msg(ExcType::TypeError, \"keywords must be strings\").into());\n            continue;\n        };\n\n        let key_str = keyword_name.as_str(interns);\n        match key_str {\n            \"sep\" => match extract_string_kwarg(value, \"sep\", heap, interns) {\n                Ok(custom_sep) => sep = custom_sep,\n                Err(e) => error = Some(e),\n            },\n            \"end\" => match extract_string_kwarg(value, \"end\", heap, interns) {\n                Ok(custom_end) => end = custom_end,\n                Err(e) => error = Some(e),\n            },\n            \"flush\" => {} // Accepted but ignored (we don't buffer output)\n            \"file\" => {\n                error = Some(\n                    SimpleException::new_msg(ExcType::TypeError, \"print() 'file' argument is not supported\").into(),\n                );\n            }\n            _ => {\n                error = Some(ExcType::type_error_unexpected_keyword(\"print\", key_str));\n            }\n        }\n    }\n\n    if let Some(error) = error {\n        Err(error)\n    } else {\n        Ok((sep, end))\n    }\n}\n\n/// Extracts a string value from a print() kwarg.\n///\n/// The kwarg can be None (returns empty string) or a string.\n/// Raises TypeError for other types.\nfn extract_string_kwarg(\n    value: &Value,\n    name: &str,\n    heap: &Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Option<String>> {\n    match value {\n        Value::None => Ok(None),\n        Value::InternString(string_id) => Ok(Some(interns.get_str(*string_id).to_owned())),\n        Value::Ref(id) => {\n            if let HeapData::Str(s) = heap.get(*id) {\n                return Ok(Some(s.as_str().to_owned()));\n            }\n            Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"{} must be None or a string, not {}\", name, value.py_type(heap)),\n            )\n            .into())\n        }\n        _ => Err(SimpleException::new_msg(\n            ExcType::TypeError,\n            format!(\"{} must be None or a string, not {}\", name, value.py_type(heap)),\n        )\n        .into()),\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/repr.rs",
    "content": "//! Implementation of the repr() builtin function.\n\nuse crate::{\n    args::ArgValues, bytecode::VM, defer_drop, exception_private::RunResult, heap::HeapData, resource::ResourceTracker,\n    types::PyTrait, value::Value,\n};\n\n/// Implementation of the repr() builtin function.\n///\n/// Returns a string containing a printable representation of an object.\npub fn builtin_repr(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"repr\", vm.heap)?;\n    defer_drop!(value, vm);\n    let heap_id = vm.heap.allocate(HeapData::Str(value.py_repr(vm).into_owned().into()))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/reversed.rs",
    "content": "//! Implementation of the reversed() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    exception_private::RunResult,\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{List, MontyIter},\n    value::Value,\n};\n\n/// Implementation of the reversed() builtin function.\n///\n/// Returns a list with elements in reverse order.\n/// Note: In Python this returns an iterator, but we return a list for simplicity.\npub fn builtin_reversed(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"reversed\", vm.heap)?;\n\n    // Collect all items\n    let mut items: Vec<_> = MontyIter::new(value, vm)?.collect(vm)?;\n\n    // Reverse in place\n    items.reverse();\n\n    let heap_id = vm.heap.allocate(HeapData::List(List::new(items)))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/round.rs",
    "content": "//! Implementation of the round() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\npub fn normalize_bool_to_int(value: Value) -> Value {\n    match value {\n        Value::Bool(b) => Value::Int(i64::from(b)),\n        other => other,\n    }\n}\n\n/// Implementation of the round() builtin function.\n///\n/// Rounds a number to a given precision in decimal digits.\n/// If ndigits is omitted or None, returns the nearest integer.\n/// Uses banker's rounding (round half to even).\npub fn builtin_round(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (number, ndigits) = args.get_one_two_args(\"round\", vm.heap)?;\n    let number = normalize_bool_to_int(number);\n    defer_drop!(number, vm);\n    defer_drop!(ndigits, vm);\n\n    // Determine the number of digits (None means round to integer)\n    // Extract digits value before potentially consuming ndigits for error handling\n    let digits: Option<i64> = match ndigits {\n        Some(Value::None) => None,\n        Some(Value::Int(n)) => Some(*n),\n        Some(Value::Bool(b)) => Some(i64::from(*b)),\n        Some(v) => {\n            let type_name = v.py_type(vm.heap);\n            return Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"'{type_name}' object cannot be interpreted as an integer\"),\n            )\n            .into());\n        }\n        None => None,\n    };\n\n    match number {\n        Value::Int(n) => {\n            if let Some(d) = digits {\n                if d >= 0 {\n                    // Positive or zero digits: return the integer unchanged\n                    Ok(Value::Int(*n))\n                } else {\n                    // Negative digits: round to tens, hundreds, etc. using banker's rounding\n                    // -d is positive since d < 0; use try_from to safely convert\n                    let exp = u32::try_from(-d).unwrap_or(u32::MAX);\n                    let factor = 10_i64.saturating_pow(exp);\n                    let rounded_f = bankers_round(*n as f64 / factor as f64);\n                    let rounded = f64_to_i64(rounded_f) * factor;\n                    Ok(Value::Int(rounded))\n                }\n            } else {\n                // No digits specified: return the integer unchanged\n                Ok(Value::Int(*n))\n            }\n        }\n        Value::Float(f) => {\n            if let Some(d) = digits {\n                // Round to `d` decimal places using banker's rounding.\n                Ok(Value::Float(round_float_to_digits(*f, d)))\n            } else {\n                // No digits: round to nearest integer and return int (banker's rounding)\n                if f.is_nan() {\n                    Err(SimpleException::new_msg(ExcType::ValueError, \"cannot convert float NaN to integer\").into())\n                } else if f.is_infinite() {\n                    Err(\n                        SimpleException::new_msg(ExcType::OverflowError, \"cannot convert float infinity to integer\")\n                            .into(),\n                    )\n                } else {\n                    Ok(Value::Int(f64_to_i64(bankers_round(*f))))\n                }\n            }\n        }\n        _ => {\n            let type_name = number.py_type(vm.heap);\n            Err(SimpleException::new_msg(\n                ExcType::TypeError,\n                format!(\"type {type_name} doesn't define __round__ method\"),\n            )\n            .into())\n        }\n    }\n}\n\n/// Implements banker's rounding (round half to even).\n///\n/// This is the rounding mode used by Python's `round()` function.\n/// When the value is exactly halfway between two integers, it rounds to the nearest even integer.\nfn bankers_round(value: f64) -> f64 {\n    let floor = value.floor();\n    let frac = value - floor;\n\n    if frac < 0.5 {\n        floor\n    } else if frac > 0.5 {\n        floor + 1.0\n    } else {\n        // Exactly 0.5 - round to even\n        if f64_to_i64(floor) % 2 == 0 { floor } else { floor + 1.0 }\n    }\n}\n\n/// Rounds a finite float to a given number of decimal digits using banker's rounding.\n///\n/// This is used for `round(x, ndigits)` where Python always returns a float.\n///\n/// For large `ndigits` values where scaling by `10**ndigits` would overflow/underflow `f64`,\n/// CPython returns either the original value (large positive `ndigits`) or a signed zero\n/// (large negative `ndigits`). We mirror that behavior and also preserve the sign of `0.0`.\nfn round_float_to_digits(value: f64, digits: i64) -> f64 {\n    if !value.is_finite() {\n        return value;\n    }\n\n    let rounded = if digits >= 0 {\n        let Ok(exp) = i32::try_from(digits) else {\n            return value;\n        };\n        let multiplier = 10_f64.powi(exp);\n        if !multiplier.is_finite() {\n            return value;\n        }\n        let scaled = value * multiplier;\n        if !scaled.is_finite() {\n            return value;\n        }\n        bankers_round(scaled) / multiplier\n    } else {\n        let Ok(exp) = i32::try_from(digits) else {\n            return 0.0_f64.copysign(value);\n        };\n        let multiplier = 10_f64.powi(exp);\n        if multiplier == 0.0 {\n            return 0.0_f64.copysign(value);\n        }\n        let scaled = value * multiplier;\n        bankers_round(scaled) / multiplier\n    };\n\n    if rounded == 0.0 {\n        0.0_f64.copysign(value)\n    } else {\n        rounded\n    }\n}\n\n/// Converts `f64` to `i64` using saturating float-to-int casting.\n///\n/// Monty uses `i64` for integer values, so float-to-int conversion must pick a\n/// bounded representation:\n/// - Values outside the `i64` range saturate to `i64::MIN`/`i64::MAX`\n/// - `NaN` converts to `0`\n///\n/// This behavior is provided by Rust's `as` casting rules for float-to-int.\nfn f64_to_i64(value: f64) -> i64 {\n    #[expect(\n        clippy::cast_possible_truncation,\n        reason = \"intentional truncation; float-to-int casts saturate and map NaN to 0\"\n    )]\n    let result = value as i64;\n    result\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/sorted.rs",
    "content": "//! Implementation of the sorted() builtin function.\n\nuse itertools::Itertools;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::{DropWithHeap, HeapData, HeapGuard},\n    resource::ResourceTracker,\n    sorting::{apply_permutation, sort_indices},\n    types::{List, MontyIter, PyTrait},\n    value::Value,\n};\n\n/// Implementation of the sorted() builtin function.\n///\n/// Returns a new sorted list from the items in an iterable.\n/// Supports `key` and `reverse` keyword arguments matching Python's\n/// `sorted(iterable, /, *, key=None, reverse=False)` signature.\npub fn builtin_sorted(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (iterable, key_fn, reverse) = parse_sorted_args(args, vm)?;\n    defer_drop!(key_fn, vm);\n\n    let items: Vec<_> = MontyIter::new(iterable, vm)?.collect(vm)?;\n    let mut items_guard = HeapGuard::new(items, vm);\n    let (items, vm) = items_guard.as_parts_mut();\n\n    {\n        // Compute key values if a key function was provided, otherwise we'll sort by the items themselves\n        let mut keys_guard;\n        let (compare_values, vm) = if let Some(f) = key_fn {\n            let keys: Vec<Value> = Vec::with_capacity(items.len());\n            // Use a HeapGuard to ensure that if key function evaluation fails partway through,\n            // we clean up any keys that were successfully computed\n            keys_guard = HeapGuard::new(keys, vm);\n            let (keys, vm) = keys_guard.as_parts_mut();\n            items\n                .iter()\n                .map(|item| {\n                    let item = item.clone_with_heap(vm);\n                    vm.evaluate_function(\"sorted() key argument\", f, ArgValues::One(item))\n                })\n                .process_results(|keys_iter| keys.extend(keys_iter))?;\n            keys_guard.as_parts()\n        } else {\n            (&*items, vm)\n        };\n\n        // Sort indices by comparing key values (or items themselves if no key)\n        let len = compare_values.len();\n        let mut indices: Vec<usize> = (0..len).collect();\n\n        sort_indices(&mut indices, compare_values, reverse, vm)?;\n\n        // Rearrange items in-place according to the sorted permutation\n        apply_permutation(items, &mut indices);\n    }\n\n    let (items, vm) = items_guard.into_parts();\n    let heap_id = vm.heap.allocate(HeapData::List(List::new(items)))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Parses the arguments for `sorted(iterable, /, *, key=None, reverse=False)`.\n///\n/// Returns `(iterable, key_fn, reverse)` where `key_fn` is `None` when no key\n/// function was provided (or `None` was explicitly passed), and `reverse` defaults\n/// to `false`.\nfn parse_sorted_args(\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Value, Option<Value>, bool)> {\n    let (mut positional, kwargs) = args.into_parts();\n\n    // Extract the single required positional argument\n    let positional_len = positional.len();\n    let Some(iterable) = positional.next() else {\n        kwargs.drop_with_heap(vm);\n        positional.drop_with_heap(vm);\n        return Err(SimpleException::new_msg(\n            ExcType::TypeError,\n            format!(\"sorted expected 1 argument, got {positional_len}\"),\n        )\n        .into());\n    };\n\n    // Reject extra positional arguments\n    if positional.len() > 0 {\n        let total = positional_len;\n        kwargs.drop_with_heap(vm);\n        iterable.drop_with_heap(vm);\n        positional.drop_with_heap(vm);\n        return Err(\n            SimpleException::new_msg(ExcType::TypeError, format!(\"sorted expected 1 argument, got {total}\")).into(),\n        );\n    }\n\n    // Parse keyword arguments: key and reverse\n    let mut iterable_guard = HeapGuard::new(iterable, vm);\n    let vm = iterable_guard.heap();\n    let (key_arg, reverse_arg) = kwargs.parse_named_kwargs_pair(\n        \"sorted\",\n        \"key\",\n        \"reverse\",\n        vm.heap,\n        vm.interns,\n        |_func_name, key_str| {\n            // CPython currently reuses the list.sort()-style wording here rather than\n            // saying \"sorted() got ...\", so match that exact user-visible message.\n            ExcType::type_error_unexpected_keyword(\"sort\", key_str)\n        },\n    )?;\n\n    // Convert reverse to bool (default false)\n    let reverse = if let Some(v) = reverse_arg {\n        let result = v.py_bool(vm);\n        v.drop_with_heap(vm);\n        result\n    } else {\n        false\n    };\n\n    // Handle key function (None means no key function)\n    let key_fn = match key_arg {\n        Some(v) if matches!(v, Value::None) => {\n            v.drop_with_heap(iterable_guard.heap());\n            None\n        }\n        other => other,\n    };\n\n    Ok((iterable_guard.into_inner(), key_fn, reverse))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/sum.rs",
    "content": "//! Implementation of the sum() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::HeapGuard,\n    resource::ResourceTracker,\n    types::{MontyIter, PyTrait, Type},\n    value::Value,\n};\n\n/// Implementation of the sum() builtin function.\n///\n/// Sums the items of an iterable from left to right with an optional start value.\n/// The default start value is 0. String start values are explicitly rejected\n/// (use `''.join(seq)` instead for string concatenation).\npub fn builtin_sum(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (iterable, start) = args.get_one_two_args(\"sum\", vm.heap)?;\n    defer_drop_mut!(start, vm);\n\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n\n    // Get the start value, defaulting to 0\n    let accumulator = match start.take() {\n        Some(v) => {\n            // Reject string start values - Python explicitly forbids this\n            if matches!(v.py_type(vm.heap), Type::Str) {\n                v.drop_with_heap(vm);\n                return Err(SimpleException::new_msg(\n                    ExcType::TypeError,\n                    \"sum() can't sum strings [use ''.join(seq) instead]\",\n                )\n                .into());\n            }\n            v\n        }\n        None => Value::Int(0),\n    };\n\n    // HeapGuard for accumulator: on success we extract it via into_inner(),\n    // on any error path it's dropped automatically\n    let mut acc_guard = HeapGuard::new(accumulator, vm);\n    let (accumulator, vm) = acc_guard.as_parts_mut();\n\n    // Sum all items\n    while let Some(item) = iter.for_next(vm)? {\n        defer_drop!(item, vm);\n\n        // Try to add the item to accumulator\n        if let Some(new_value) = accumulator.py_add(item, vm)? {\n            // Replace the old accumulator with the new value, dropping the old one\n            let old = std::mem::replace(accumulator, new_value);\n            old.drop_with_heap(vm);\n        } else {\n            // Types don't support addition\n            let acc_type = accumulator.py_type(vm.heap);\n            let item_type = item.py_type(vm.heap);\n            return Err(ExcType::binary_type_error(\"+\", acc_type, item_type));\n        }\n    }\n\n    Ok(acc_guard.into_inner())\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/type_.rs",
    "content": "//! Implementation of the type() builtin function.\n\nuse super::Builtins;\nuse crate::{\n    args::ArgValues, bytecode::VM, defer_drop, exception_private::RunResult, resource::ResourceTracker, types::PyTrait,\n    value::Value,\n};\n\n/// Implementation of the type() builtin function.\n///\n/// Returns the type of an object.\npub fn builtin_type(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"type\", vm.heap)?;\n    defer_drop!(value, vm);\n    Ok(Value::Builtin(Builtins::Type(value.py_type(vm.heap))))\n}\n"
  },
  {
    "path": "crates/monty/src/builtins/zip.rs",
    "content": "//! Implementation of the zip() builtin function.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop_mut,\n    exception_private::RunResult,\n    heap::HeapData,\n    resource::ResourceTracker,\n    types::{List, MontyIter, allocate_tuple, tuple::TupleVec},\n    value::Value,\n};\n\n/// Implementation of the zip() builtin function.\n///\n/// Returns a list of tuples, where the i-th tuple contains the i-th element\n/// from each of the argument iterables. Stops when the shortest iterable is exhausted.\n/// Note: In Python this returns an iterator, but we return a list for simplicity.\npub fn builtin_zip(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (positional, kwargs) = args.into_parts();\n    defer_drop_mut!(positional, vm);\n\n    // TODO: support kwargs (strict)\n    kwargs.not_supported_yet(\"zip\", vm.heap)?;\n\n    if positional.len() == 0 {\n        // zip() with no arguments returns empty list\n        let heap_id = vm.heap.allocate(HeapData::List(List::new(Vec::new())))?;\n        return Ok(Value::Ref(heap_id));\n    }\n\n    // Create iterators for each iterable\n    let mut iterators: Vec<MontyIter> = Vec::with_capacity(positional.len());\n    for iterable in positional {\n        match MontyIter::new(iterable, vm) {\n            Ok(iter) => iterators.push(iter),\n            Err(e) => {\n                // Clean up already-created iterators\n                for iter in iterators {\n                    iter.drop_with_heap(vm);\n                }\n                return Err(e);\n            }\n        }\n    }\n\n    let mut result: Vec<Value> = Vec::new();\n\n    // Zip until shortest iterator is exhausted\n    'outer: loop {\n        let mut tuple_items = TupleVec::with_capacity(iterators.len());\n\n        for iter in &mut iterators {\n            if let Some(item) = iter.for_next(vm)? {\n                tuple_items.push(item);\n            } else {\n                // This iterator is exhausted - drop partial tuple items and stop\n                for item in tuple_items {\n                    item.drop_with_heap(vm);\n                }\n                break 'outer;\n            }\n        }\n\n        // Create tuple from collected items\n        let tuple_val = allocate_tuple(tuple_items, vm.heap)?;\n        result.push(tuple_val);\n    }\n\n    // Clean up iterators\n    for iter in iterators {\n        iter.drop_with_heap(vm);\n    }\n\n    let heap_id = vm.heap.allocate(HeapData::List(List::new(result)))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/builder.rs",
    "content": "//! Builder for emitting bytecode during compilation.\n//!\n//! `CodeBuilder` provides methods for emitting opcodes and operands, handling\n//! forward jumps with patching, and tracking source locations for tracebacks.\n\nuse std::collections::HashSet;\n\nuse super::{\n    code::{Code, ConstPool, ExceptionEntry, LocationEntry},\n    op::Opcode,\n};\nuse crate::{intern::StringId, parse::CodeRange, value::Value};\n\n/// Builder for emitting bytecode during compilation.\n///\n/// Handles encoding opcodes and operands into raw bytes, managing forward jumps\n/// that need patching, and tracking source locations for traceback generation.\n///\n/// # Usage\n///\n/// ```ignore\n/// let mut builder = CodeBuilder::new();\n/// builder.set_location(some_range, None);\n/// builder.emit(Opcode::LoadNone);\n/// builder.emit_u8(Opcode::LoadLocal, 0);\n/// let jump = builder.emit_jump(Opcode::JumpIfFalse);\n/// // ... emit more code ...\n/// builder.patch_jump(jump);\n/// let code = builder.build(num_locals);\n/// ```\n#[derive(Debug, Default)]\npub struct CodeBuilder {\n    /// The bytecode being built.\n    bytecode: Vec<u8>,\n\n    /// Constants collected during compilation.\n    constants: Vec<Value>,\n\n    /// Source location entries for traceback generation.\n    location_table: Vec<LocationEntry>,\n\n    /// Exception handler entries.\n    exception_table: Vec<ExceptionEntry>,\n\n    /// Current source location (set before emitting instructions).\n    current_location: Option<CodeRange>,\n\n    /// Current focus location within the source range.\n    current_focus: Option<CodeRange>,\n\n    /// Current stack depth for tracking max stack usage.\n    current_stack_depth: u16,\n\n    /// Maximum stack depth seen during compilation.\n    max_stack_depth: u16,\n\n    /// Local variable names indexed by slot number.\n    ///\n    /// Populated during compilation to enable proper NameError messages\n    /// when accessing undefined local variables.\n    local_names: Vec<Option<StringId>>,\n\n    /// Local variable slots that are assigned somewhere in this function.\n    ///\n    /// Used to determine whether to raise `UnboundLocalError` or `NameError`\n    /// when loading an undefined local variable.\n    assigned_locals: HashSet<u16>,\n}\n\nimpl CodeBuilder {\n    /// Creates a new empty CodeBuilder.\n    #[must_use]\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Sets the current source location for subsequent instructions.\n    ///\n    /// This location will be recorded in the location table when the next\n    /// instruction is emitted. Call this before emitting instructions that\n    /// correspond to source code.\n    pub fn set_location(&mut self, range: CodeRange, focus: Option<CodeRange>) {\n        self.current_location = Some(range);\n        self.current_focus = focus;\n    }\n\n    /// Emits a no-operand instruction and updates stack depth tracking.\n    pub fn emit(&mut self, op: Opcode) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        // Track stack effect for opcodes with known fixed effects\n        if let Some(effect) = op.stack_effect() {\n            self.adjust_stack(effect);\n        }\n    }\n\n    /// Emits an instruction with a u8 operand and updates stack depth tracking.\n    pub fn emit_u8(&mut self, op: Opcode, operand: u8) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        self.bytecode.push(operand);\n        // Track stack effect - some need operand-based calculation\n        self.track_stack_effect_u8(op, operand);\n    }\n\n    /// Emits an instruction with an i8 operand and updates stack depth tracking.\n    pub fn emit_i8(&mut self, op: Opcode, operand: i8) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        // Reinterpret i8 as u8 for bytecode encoding\n        self.bytecode.push(operand.to_ne_bytes()[0]);\n        // Track stack effect for opcodes with known fixed effects\n        if let Some(effect) = op.stack_effect() {\n            self.adjust_stack(effect);\n        }\n    }\n\n    /// Emits an instruction with two u8 operands and updates stack depth tracking.\n    ///\n    /// Used for UnpackEx: before_count (u8) + after_count (u8)\n    pub fn emit_u8_u8(&mut self, op: Opcode, operand1: u8, operand2: u8) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        self.bytecode.push(operand1);\n        self.bytecode.push(operand2);\n        // UnpackEx: pops 1, pushes (before + 1 + after) = before + after + 1\n        // Net effect: before + after\n        if op == Opcode::UnpackEx {\n            self.adjust_stack(i16::from(operand1) + i16::from(operand2));\n        } else if let Some(effect) = op.stack_effect() {\n            self.adjust_stack(effect);\n        }\n    }\n\n    /// Emits an instruction with a u16 operand (little-endian) and updates stack depth tracking.\n    pub fn emit_u16(&mut self, op: Opcode, operand: u16) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        self.bytecode.extend_from_slice(&operand.to_le_bytes());\n        // Track stack effect - some need operand-based calculation\n        self.track_stack_effect_u16(op, operand);\n    }\n\n    /// Emits an instruction with a u16 operand followed by a u8 operand.\n    ///\n    /// Used for MakeFunction: func_id (u16) + defaults_count (u8)\n    /// Used for CallAttr: attr_name_id (u16) + arg_count (u8)\n    pub fn emit_u16_u8(&mut self, op: Opcode, operand1: u16, operand2: u8) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        self.bytecode.extend_from_slice(&operand1.to_le_bytes());\n        self.bytecode.push(operand2);\n        // Track stack effects based on opcode\n        match op {\n            Opcode::MakeFunction => {\n                // pops defaults_count defaults, pushes function: 1 - defaults_count\n                self.adjust_stack(1 - i16::from(operand2));\n            }\n            Opcode::CallAttr => {\n                // pops obj + args, pushes result: 1 - (1 + arg_count) = -arg_count\n                self.adjust_stack(-i16::from(operand2));\n            }\n            _ => {\n                if let Some(effect) = op.stack_effect() {\n                    self.adjust_stack(effect);\n                }\n            }\n        }\n    }\n\n    /// Emits an instruction with a u16 operand followed by two u8 operands.\n    ///\n    /// Used for MakeClosure: func_id (u16) + defaults_count (u8) + cell_count (u8)\n    pub fn emit_u16_u8_u8(&mut self, op: Opcode, operand1: u16, operand2: u8, operand3: u8) {\n        self.record_location();\n        self.bytecode.push(op as u8);\n        self.bytecode.extend_from_slice(&operand1.to_le_bytes());\n        self.bytecode.push(operand2);\n        self.bytecode.push(operand3);\n        // MakeClosure: pops defaults_count defaults, pushes closure\n        // Cell values are captured from locals, not popped from stack\n        // Stack effect: 1 - defaults_count\n        if op == Opcode::MakeClosure {\n            self.adjust_stack(1 - i16::from(operand2));\n        } else if let Some(effect) = op.stack_effect() {\n            self.adjust_stack(effect);\n        }\n    }\n\n    /// Emits `CallBuiltinFunction` instruction.\n    ///\n    /// Operands: builtin_id (u8) + arg_count (u8)\n    ///\n    /// The builtin_id is the `#[repr(u8)]` discriminant of `BuiltinsFunctions`.\n    /// This is an optimization that avoids constant pool lookup and stack manipulation.\n    pub fn emit_call_builtin_function(&mut self, builtin_id: u8, arg_count: u8) {\n        self.record_location();\n        self.bytecode.push(Opcode::CallBuiltinFunction as u8);\n        self.bytecode.push(builtin_id);\n        self.bytecode.push(arg_count);\n        // CallBuiltinFunction: pops args, pushes result. No callable on stack.\n        // Stack effect: 1 - arg_count\n        self.adjust_stack(1 - i16::from(arg_count));\n    }\n\n    /// Emits `CallBuiltinType` instruction.\n    ///\n    /// Operands: type_id (u8) + arg_count (u8)\n    ///\n    /// The type_id is the `#[repr(u8)]` discriminant of `BuiltinsTypes`.\n    /// This is an optimization for type constructors like `list()`, `int()`, `str()`.\n    pub fn emit_call_builtin_type(&mut self, type_id: u8, arg_count: u8) {\n        self.record_location();\n        self.bytecode.push(Opcode::CallBuiltinType as u8);\n        self.bytecode.push(type_id);\n        self.bytecode.push(arg_count);\n        // CallBuiltinType: pops args, pushes result. No callable on stack.\n        // Stack effect: 1 - arg_count\n        self.adjust_stack(1 - i16::from(arg_count));\n    }\n\n    /// Emits CallFunctionKw with inline keyword names.\n    ///\n    /// Operands: pos_count (u8) + kw_count (u8) + kw_count * name_id (u16 each)\n    ///\n    /// The kwname_ids slice contains StringId indices for each keyword argument\n    /// name, in order matching how the values were pushed to the stack.\n    pub fn emit_call_function_kw(&mut self, pos_count: u8, kwname_ids: &[u16]) {\n        self.record_location();\n        self.bytecode.push(Opcode::CallFunctionKw as u8);\n        self.bytecode.push(pos_count);\n        self.bytecode\n            .push(u8::try_from(kwname_ids.len()).expect(\"keyword count exceeds u8\"));\n        for &name_id in kwname_ids {\n            self.bytecode.extend_from_slice(&name_id.to_le_bytes());\n        }\n        // CallFunctionKw: pops callable + pos_args + kw_args, pushes result\n        // Stack effect: 1 - (1 + pos_count + kw_count) = -pos_count - kw_count\n        let kw_count = i16::try_from(kwname_ids.len()).expect(\"keyword count exceeds i16\");\n        let total_args = i16::from(pos_count) + kw_count;\n        self.adjust_stack(-total_args);\n    }\n\n    /// Emits CallAttrKw with inline keyword names.\n    ///\n    /// Operands: attr_name_id (u16) + pos_count (u8) + kw_count (u8) + kw_count * name_id (u16 each)\n    ///\n    /// The kwname_ids slice contains StringId indices for each keyword argument\n    /// name, in order matching how the values were pushed to the stack.\n    pub fn emit_call_attr_kw(&mut self, attr_name_id: u16, pos_count: u8, kwname_ids: &[u16]) {\n        self.record_location();\n        self.bytecode.push(Opcode::CallAttrKw as u8);\n        self.bytecode.extend_from_slice(&attr_name_id.to_le_bytes());\n        self.bytecode.push(pos_count);\n        self.bytecode\n            .push(u8::try_from(kwname_ids.len()).expect(\"keyword count exceeds u8\"));\n        for &name_id in kwname_ids {\n            self.bytecode.extend_from_slice(&name_id.to_le_bytes());\n        }\n        // CallAttrKw: pops obj + pos_args + kw_args, pushes result\n        // Stack effect: 1 - (1 + pos_count + kw_count) = -pos_count - kw_count\n        let kw_count = i16::try_from(kwname_ids.len()).expect(\"keyword count exceeds i16\");\n        let total_args = i16::from(pos_count) + kw_count;\n        self.adjust_stack(-total_args);\n    }\n\n    /// Emits a forward jump instruction, returning a label to patch later.\n    ///\n    /// The jump offset is initially set to 0 and must be patched with\n    /// `patch_jump()` once the target location is known.\n    #[must_use]\n    pub fn emit_jump(&mut self, op: Opcode) -> JumpLabel {\n        self.record_location();\n        let label = JumpLabel(self.bytecode.len());\n        self.bytecode.push(op as u8);\n        // Placeholder for i16 offset (will be patched)\n        self.bytecode.extend_from_slice(&0i16.to_le_bytes());\n        // Track stack effect\n        match op {\n            // ForIter: when successful (not jumping), pushes next value (+1)\n            // When exhausted (jumping), pops iterator (-1), but that's after loop\n            Opcode::ForIter => self.adjust_stack(1),\n            // JumpIfTrueOrPop/JumpIfFalseOrPop: pops when not jumping (fallthrough)\n            Opcode::JumpIfTrueOrPop | Opcode::JumpIfFalseOrPop => self.adjust_stack(-1),\n            _ => {\n                if let Some(effect) = op.stack_effect() {\n                    self.adjust_stack(effect);\n                }\n            }\n        }\n        label\n    }\n\n    /// Patches a forward jump to point to the current bytecode location.\n    ///\n    /// The offset is calculated relative to the position after the jump\n    /// instruction's operand (i.e., where execution would continue if\n    /// the jump is not taken).\n    ///\n    /// # Panics\n    ///\n    /// Panics if the jump offset exceeds i16 range (-32768..32767), which\n    /// indicates the function is too large. This is a compile-time error\n    /// rather than silent truncation.\n    pub fn patch_jump(&mut self, label: JumpLabel) {\n        let target = self.bytecode.len();\n        // Offset is relative to position after the jump instruction (opcode + i16 = 3 bytes)\n        let target_i64 = i64::try_from(target).expect(\"bytecode target exceeds i64\");\n        let label_i64 = i64::try_from(label.0).expect(\"bytecode label exceeds i64\");\n        let raw_offset = target_i64 - label_i64 - 3;\n        let offset =\n            i16::try_from(raw_offset).expect(\"jump offset exceeds i16 range (-32768..32767); function too large\");\n        let bytes = offset.to_le_bytes();\n        self.bytecode[label.0 + 1] = bytes[0];\n        self.bytecode[label.0 + 2] = bytes[1];\n    }\n\n    /// Emits a backward jump to a known target offset.\n    ///\n    /// Unlike forward jumps, backward jumps have a known target at emit time,\n    /// so no patching is needed.\n    pub fn emit_jump_to(&mut self, op: Opcode, target: usize) {\n        self.record_location();\n        let current = self.bytecode.len();\n        // Offset is relative to position after this instruction (current + 3)\n        let target_i64 = i64::try_from(target).expect(\"bytecode target exceeds i64\");\n        let current_i64 = i64::try_from(current).expect(\"bytecode offset exceeds i64\");\n        let raw_offset = target_i64 - (current_i64 + 3);\n        let offset =\n            i16::try_from(raw_offset).expect(\"jump offset exceeds i16 range (-32768..32767); function too large\");\n        self.bytecode.push(op as u8);\n        self.bytecode.extend_from_slice(&offset.to_le_bytes());\n        // Track stack effect (jump instructions pop condition)\n        if let Some(effect) = op.stack_effect() {\n            self.adjust_stack(effect);\n        }\n    }\n\n    /// Returns the current bytecode offset.\n    ///\n    /// Use this to record loop start positions for backward jumps.\n    #[must_use]\n    pub fn current_offset(&self) -> usize {\n        self.bytecode.len()\n    }\n\n    /// Emits `LoadLocal`, using specialized opcodes for slots 0-3.\n    ///\n    /// Slots 0-3 use zero-operand opcodes (`LoadLocal0`, etc.) for efficiency.\n    /// Slots 4-255 use `LoadLocal` with a u8 operand.\n    /// Slots 256+ use `LoadLocalW` with a u16 operand.\n    /// Registers a local variable name for a given slot.\n    ///\n    /// This is called during compilation when we encounter a variable access.\n    /// The name is used to generate proper NameError messages.\n    pub fn register_local_name(&mut self, slot: u16, name: StringId) {\n        let slot_idx = slot as usize;\n        // Extend the vector if needed\n        if slot_idx >= self.local_names.len() {\n            self.local_names.resize(slot_idx + 1, None);\n        }\n        // Only set if not already set (first occurrence determines the name)\n        if self.local_names[slot_idx].is_none() {\n            self.local_names[slot_idx] = Some(name);\n        }\n    }\n\n    /// Registers a local variable slot as \"assigned\" (vs undefined reference).\n    ///\n    /// Called during compilation for variables that are assigned somewhere in the function.\n    /// Used at runtime to determine whether to raise `UnboundLocalError` (assigned local\n    /// accessed before assignment) or `NameError` (name doesn't exist anywhere).\n    pub fn register_assigned_local(&mut self, slot: u16) {\n        self.assigned_locals.insert(slot);\n    }\n\n    /// Emits a `LoadLocal` instruction, using specialized variants for common slots.\n    pub fn emit_load_local(&mut self, slot: u16) {\n        match slot {\n            0 => self.emit(Opcode::LoadLocal0),\n            1 => self.emit(Opcode::LoadLocal1),\n            2 => self.emit(Opcode::LoadLocal2),\n            3 => self.emit(Opcode::LoadLocal3),\n            _ => {\n                if let Ok(s) = u8::try_from(slot) {\n                    self.emit_u8(Opcode::LoadLocal, s);\n                } else {\n                    self.emit_u16(Opcode::LoadLocalW, slot);\n                }\n            }\n        }\n    }\n\n    /// Emits a `LoadLocalCallable` instruction for call-context loads.\n    ///\n    /// Unlike `emit_load_local`, this does NOT use specialized 0-3 variants since\n    /// external function calls are rare enough that the optimization isn't worth\n    /// the extra opcode slots. The `name_id` is encoded directly in the operand\n    /// to avoid needing to look up the name from the code's local_names array.\n    pub fn emit_load_local_callable(&mut self, slot: u16, name_id: StringId) {\n        let name_id_u16 = u16::try_from(name_id.index()).expect(\"name_id exceeds u16\");\n        if let Ok(s) = u8::try_from(slot) {\n            // Emit LoadLocalCallable with u8 slot + u16 name_id\n            self.record_location();\n            self.bytecode.push(Opcode::LoadLocalCallable as u8);\n            self.bytecode.push(s);\n            self.bytecode.extend_from_slice(&name_id_u16.to_le_bytes());\n            self.adjust_stack(1);\n        } else {\n            // Emit LoadLocalCallableW with u16 slot + u16 name_id\n            self.record_location();\n            self.bytecode.push(Opcode::LoadLocalCallableW as u8);\n            self.bytecode.extend_from_slice(&slot.to_le_bytes());\n            self.bytecode.extend_from_slice(&name_id_u16.to_le_bytes());\n            self.adjust_stack(1);\n        }\n    }\n\n    /// Emits a `LoadGlobalCallable` instruction for call-context loads.\n    ///\n    /// The `name_id` is encoded directly in the operand to avoid the ambiguity\n    /// of looking up global names from a function's local_names array (global slots\n    /// and local slots use different namespaces).\n    pub fn emit_load_global_callable(&mut self, slot: u16, name_id: StringId) {\n        let name_id_u16 = u16::try_from(name_id.index()).expect(\"name_id exceeds u16\");\n        self.record_location();\n        self.bytecode.push(Opcode::LoadGlobalCallable as u8);\n        self.bytecode.extend_from_slice(&slot.to_le_bytes());\n        self.bytecode.extend_from_slice(&name_id_u16.to_le_bytes());\n        self.adjust_stack(1);\n    }\n\n    /// Emits `StoreLocal`, using wide variant for slots > 255.\n    pub fn emit_store_local(&mut self, slot: u16) {\n        if let Ok(s) = u8::try_from(slot) {\n            self.emit_u8(Opcode::StoreLocal, s);\n        } else {\n            self.emit_u16(Opcode::StoreLocalW, slot);\n        }\n    }\n\n    /// Adds a constant to the pool, returning its index.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the constant pool exceeds 65535 entries. This is a compile-time\n    /// error indicating the function has too many constants.\n    #[must_use]\n    pub fn add_const(&mut self, value: Value) -> u16 {\n        let idx = self.constants.len();\n        let idx_u16 = u16::try_from(idx).expect(\"constant pool exceeds u16 range (65535); too many constants\");\n        self.constants.push(value);\n        idx_u16\n    }\n\n    /// Adds an exception handler entry.\n    ///\n    /// Entries should be added in innermost-first order for nested try blocks.\n    pub fn add_exception_entry(&mut self, entry: ExceptionEntry) {\n        self.exception_table.push(entry);\n    }\n\n    /// Returns the current tracked stack depth.\n    #[must_use]\n    pub fn stack_depth(&self) -> u16 {\n        self.current_stack_depth\n    }\n\n    /// Builds the final Code object.\n    ///\n    /// Consumes the builder and returns a Code object containing the\n    /// compiled bytecode and all metadata.\n    #[must_use]\n    pub fn build(self, num_locals: u16) -> Code {\n        // Convert local_names from Vec<Option<StringId>> to Vec<StringId>,\n        // using StringId::default() for slots with no recorded name\n        let local_names: Vec<StringId> = self.local_names.into_iter().map(Option::unwrap_or_default).collect();\n\n        Code::new(\n            self.bytecode,\n            ConstPool::from_vec(self.constants),\n            self.location_table,\n            self.exception_table,\n            num_locals,\n            self.max_stack_depth,\n            local_names,\n            self.assigned_locals,\n        )\n    }\n\n    /// Records the current location in the location table if set.\n    fn record_location(&mut self) {\n        if let Some(range) = self.current_location {\n            let offset = u32::try_from(self.bytecode.len()).expect(\"bytecode length exceeds u32\");\n            self.location_table\n                .push(LocationEntry::new(offset, range, self.current_focus));\n        }\n    }\n\n    /// Sets the current stack depth to an absolute value.\n    ///\n    /// Used when compiling code paths that branch and reconverge with different\n    /// stack states (e.g., break/continue through finally blocks).\n    /// Updates `max_stack_depth` if the new depth exceeds it.\n    pub fn set_stack_depth(&mut self, depth: u16) {\n        self.current_stack_depth = depth;\n        self.max_stack_depth = self.max_stack_depth.max(depth);\n    }\n\n    /// Adjusts the stack depth by the given delta.\n    ///\n    /// Positive values indicate pushes, negative values indicate pops.\n    /// Updates `max_stack_depth` if the new depth exceeds it.\n    fn adjust_stack(&mut self, delta: i16) {\n        let new_depth = i32::from(self.current_stack_depth) + i32::from(delta);\n        // Stack depth shouldn't go negative (indicates compiler bug)\n        debug_assert!(new_depth >= 0, \"Stack depth went negative: {new_depth}\");\n        // Safe cast: new_depth is non-negative and stack won't exceed u16::MAX in practice\n        self.current_stack_depth = u16::try_from(new_depth.max(0)).unwrap_or(u16::MAX);\n        self.max_stack_depth = self.max_stack_depth.max(self.current_stack_depth);\n    }\n\n    /// Tracks stack effect for opcodes with u8 operand.\n    ///\n    /// For opcodes with variable effects (like `CallFunction`, `BuildList`),\n    /// calculates the effect based on the operand.\n    fn track_stack_effect_u8(&mut self, op: Opcode, operand: u8) {\n        let effect: i16 = match op {\n            // CallFunction pops (callable + args), pushes result: -(1 + arg_count) + 1 = -arg_count\n            Opcode::CallFunction => -i16::from(operand),\n            // UnpackSequence pops 1, pushes n: n - 1\n            Opcode::UnpackSequence => i16::from(operand) - 1,\n            // ListAppend/SetAdd pop value: -1 (depth operand doesn't affect stack count)\n            Opcode::ListAppend | Opcode::SetAdd => -1,\n            // DictSetItem pops key and value: -2\n            Opcode::DictSetItem => -2,\n            // Default: use fixed effect if available\n            _ => op.stack_effect().unwrap_or(0),\n        };\n        self.adjust_stack(effect);\n    }\n\n    /// Tracks stack effect for opcodes with u16 operand.\n    ///\n    /// For opcodes with variable effects (like `BuildList`, `BuildTuple`),\n    /// calculates the effect based on the operand.\n    fn track_stack_effect_u16(&mut self, op: Opcode, operand: u16) {\n        // Safe cast: operand won't exceed i16::MAX in practice (would be a huge list)\n        let operand_i16 = operand.cast_signed();\n        let effect: i16 = match op {\n            // BuildList/BuildTuple/BuildSet: pop n, push 1: -(n - 1) = 1 - n\n            Opcode::BuildList | Opcode::BuildTuple | Opcode::BuildSet => 1 - operand_i16,\n            // BuildDict: pop 2n (key-value pairs), push 1: 1 - 2n\n            Opcode::BuildDict => 1 - 2 * operand_i16,\n            // BuildFString: pop n parts, push 1: 1 - n\n            Opcode::BuildFString => 1 - operand_i16,\n            // Default: use fixed effect if available\n            _ => op.stack_effect().unwrap_or(0),\n        };\n        self.adjust_stack(effect);\n    }\n\n    /// Manually adjust stack depth for complex scenarios.\n    ///\n    /// Use this when the compiler knows the exact stack effect that can't\n    /// be determined from the opcode alone (e.g., exception handlers pushing\n    /// an exception value).\n    pub fn adjust_stack_depth(&mut self, delta: i16) {\n        self.adjust_stack(delta);\n    }\n}\n\n/// Label for a forward jump that needs patching.\n///\n/// Stores the bytecode offset where the jump instruction was emitted.\n/// Pass this to `patch_jump()` once the target location is known.\n#[derive(Debug, Clone, Copy)]\npub struct JumpLabel(usize);\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_emit_basic() {\n        let mut builder = CodeBuilder::new();\n        builder.emit(Opcode::LoadNone);\n        builder.emit(Opcode::Pop);\n\n        let code = builder.build(0);\n        assert_eq!(code.bytecode(), &[Opcode::LoadNone as u8, Opcode::Pop as u8]);\n    }\n\n    #[test]\n    fn test_emit_u8_operand() {\n        let mut builder = CodeBuilder::new();\n        builder.emit_u8(Opcode::LoadLocal, 42);\n\n        let code = builder.build(0);\n        assert_eq!(code.bytecode(), &[Opcode::LoadLocal as u8, 42]);\n    }\n\n    #[test]\n    fn test_emit_u16_operand() {\n        let mut builder = CodeBuilder::new();\n        builder.emit_u16(Opcode::LoadConst, 0x1234);\n\n        let code = builder.build(0);\n        assert_eq!(code.bytecode(), &[Opcode::LoadConst as u8, 0x34, 0x12]);\n    }\n\n    #[test]\n    fn test_forward_jump() {\n        let mut builder = CodeBuilder::new();\n        let jump = builder.emit_jump(Opcode::Jump);\n        builder.emit(Opcode::LoadNone); // 1 byte, skipped by jump\n        builder.emit(Opcode::LoadNone); // 1 byte, skipped by jump\n        builder.patch_jump(jump);\n        builder.emit(Opcode::LoadNone); // Return value\n        builder.emit(Opcode::ReturnValue);\n\n        let code = builder.build(0);\n        // Jump at offset 0, target at offset 5 (after 2x LoadNone)\n        // Offset = 5 - 0 - 3 = 2\n        assert_eq!(\n            code.bytecode(),\n            &[\n                Opcode::Jump as u8,\n                2,\n                0, // i16 little-endian = 2\n                Opcode::LoadNone as u8,\n                Opcode::LoadNone as u8,\n                Opcode::LoadNone as u8,\n                Opcode::ReturnValue as u8,\n            ]\n        );\n    }\n\n    #[test]\n    fn test_backward_jump() {\n        let mut builder = CodeBuilder::new();\n        let loop_start = builder.current_offset();\n        builder.emit(Opcode::LoadNone); // offset 0, 1 byte\n        builder.emit(Opcode::Pop); // offset 1, 1 byte\n        builder.emit_jump_to(Opcode::Jump, loop_start); // offset 2, target 0\n\n        let code = builder.build(0);\n        // Jump at offset 2, target at offset 0\n        // Offset = 0 - (2 + 3) = -5\n        let expected_offset = (-5i16).to_le_bytes();\n        assert_eq!(\n            code.bytecode(),\n            &[\n                Opcode::LoadNone as u8,\n                Opcode::Pop as u8,\n                Opcode::Jump as u8,\n                expected_offset[0],\n                expected_offset[1],\n            ]\n        );\n    }\n\n    #[test]\n    fn test_load_local_specialization() {\n        let mut builder = CodeBuilder::new();\n        builder.emit_load_local(0);\n        builder.emit_load_local(1);\n        builder.emit_load_local(2);\n        builder.emit_load_local(3);\n        builder.emit_load_local(4);\n        builder.emit_load_local(256);\n\n        let code = builder.build(0);\n        assert_eq!(\n            code.bytecode(),\n            &[\n                Opcode::LoadLocal0 as u8,\n                Opcode::LoadLocal1 as u8,\n                Opcode::LoadLocal2 as u8,\n                Opcode::LoadLocal3 as u8,\n                Opcode::LoadLocal as u8,\n                4,\n                Opcode::LoadLocalW as u8,\n                0,\n                1, // 256 in little-endian\n            ]\n        );\n    }\n\n    #[test]\n    fn test_add_const() {\n        let mut builder = CodeBuilder::new();\n        let idx1 = builder.add_const(Value::Int(42));\n        let idx2 = builder.add_const(Value::None);\n\n        assert_eq!(idx1, 0);\n        assert_eq!(idx2, 1);\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/code.rs",
    "content": "//! Code object containing compiled bytecode and metadata.\n//!\n//! A `Code` object represents a compiled function or module. It contains the raw\n//! bytecode instructions, a constant pool, source location information for tracebacks,\n//! and an exception handler table.\n\nuse std::collections::HashSet;\n\nuse crate::{intern::StringId, parse::CodeRange, value::Value};\n\n/// Compiled bytecode for a function or module.\n///\n/// This is the output of the bytecode compiler and the input to the VM.\n/// Each function has its own Code object; module-level code also gets one.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct Code {\n    /// Raw bytecode instructions as a byte vector.\n    ///\n    /// Opcodes are 1 byte each, followed by their operands (0-3 bytes depending\n    /// on the instruction). The variable-width encoding gives better cache locality\n    /// than fixed-width alternatives.\n    bytecode: Vec<u8>,\n\n    /// Constant pool for this code object.\n    ///\n    /// Values referenced by `LoadConst` instructions. Includes numbers, strings\n    /// (as `Value::InternString`), and other literal values.\n    constants: ConstPool,\n\n    /// Source location table for tracebacks.\n    ///\n    /// Maps bytecode offsets to source locations. Used to generate Python-style\n    /// tracebacks with line numbers and caret markers when exceptions occur.\n    location_table: Vec<LocationEntry>,\n\n    /// Exception handler table.\n    ///\n    /// Maps protected bytecode ranges to their exception handlers. Consulted when\n    /// an exception is raised to find the appropriate handler. Entries are ordered\n    /// innermost-first for nested try blocks.\n    exception_table: Vec<ExceptionEntry>,\n\n    /// Number of local variables (namespace slots needed).\n    ///\n    /// Used to pre-allocate the namespace when entering this code.\n    num_locals: u16,\n\n    /// Maximum stack depth needed during execution.\n    ///\n    /// Used as a hint for pre-allocating the operand stack. Computed during\n    /// compilation by tracking push/pop operations.\n    stack_size: u16,\n\n    /// Local variable names for error messages.\n    ///\n    /// Maps slot indices to variable names. Used to generate proper NameError\n    /// messages when accessing undefined local variables (e.g., \"name 'x' is not defined\").\n    local_names: Vec<StringId>,\n\n    /// Local variable slots that are assigned somewhere in this function.\n    ///\n    /// Used to determine whether to raise `UnboundLocalError` (slot is assigned somewhere\n    /// but accessed before assignment) or `NameError` (name doesn't exist in any scope).\n    assigned_locals: HashSet<u16>,\n}\n\nimpl Code {\n    /// Creates a new Code object with all components.\n    ///\n    /// This is typically called by `CodeBuilder::build()` after compilation.\n    #[must_use]\n    #[expect(clippy::too_many_arguments)]\n    pub fn new(\n        bytecode: Vec<u8>,\n        constants: ConstPool,\n        location_table: Vec<LocationEntry>,\n        exception_table: Vec<ExceptionEntry>,\n        num_locals: u16,\n        stack_size: u16,\n        local_names: Vec<StringId>,\n        assigned_locals: HashSet<u16>,\n    ) -> Self {\n        Self {\n            bytecode,\n            constants,\n            location_table,\n            exception_table,\n            num_locals,\n            stack_size,\n            local_names,\n            assigned_locals,\n        }\n    }\n\n    /// Returns the raw bytecode bytes.\n    #[must_use]\n    pub fn bytecode(&self) -> &[u8] {\n        &self.bytecode\n    }\n\n    /// Returns the constant pool.\n    #[must_use]\n    pub fn constants(&self) -> &ConstPool {\n        &self.constants\n    }\n\n    /// Returns the local variable name for a given slot index.\n    ///\n    /// Used to generate proper NameError messages when accessing undefined locals.\n    #[must_use]\n    pub fn local_name(&self, slot: u16) -> Option<StringId> {\n        self.local_names.get(slot as usize).copied()\n    }\n\n    /// Returns whether the slot is an assigned local (vs an undefined reference).\n    ///\n    /// Used to determine whether to raise `UnboundLocalError` (true) or `NameError` (false)\n    /// when loading an undefined local variable.\n    #[must_use]\n    pub fn is_assigned_local(&self, slot: u16) -> bool {\n        self.assigned_locals.contains(&slot)\n    }\n\n    /// Finds the location entry for a given bytecode offset.\n    ///\n    /// Location entries are recorded at instruction boundaries. This method finds\n    /// the most recent entry at or before the given offset.\n    ///\n    /// Returns `None` if the location table is empty or the offset is before\n    /// the first recorded location.\n    #[must_use]\n    pub fn location_for_offset(&self, offset: usize) -> Option<&LocationEntry> {\n        // Location entries are in order by bytecode offset.\n        // Find the last entry where bytecode_offset <= offset.\n        let offset_u32 = u32::try_from(offset).expect(\"bytecode offset exceeds u32\");\n        self.location_table\n            .iter()\n            .rev()\n            .find(|entry| entry.bytecode_offset <= offset_u32)\n    }\n\n    /// Finds an exception handler for the given bytecode offset.\n    ///\n    /// Searches the exception table for an entry whose protected range contains\n    /// the given offset. Returns the first (innermost) matching handler, since\n    /// entries are ordered innermost-first for nested try blocks.\n    ///\n    /// Returns `None` if no handler covers this offset.\n    #[must_use]\n    pub fn find_exception_handler(&self, offset: u32) -> Option<&ExceptionEntry> {\n        self.exception_table.iter().find(|entry| entry.contains(offset))\n    }\n}\n\n/// TODO remove, this doesn't add any value\n/// Constant pool for a code object.\n///\n/// Stores literal values referenced by `LoadConst` instructions. Strings are stored\n/// as `Value::InternString(StringId)` pointing to the global `Interns` table, not\n/// duplicated here. At runtime, constants are loaded via `clone_with_heap()` to\n/// handle reference counting properly.\n#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]\npub(crate) struct ConstPool {\n    /// The constant values, indexed by the operand of `LoadConst`.\n    values: Vec<Value>,\n}\n\nimpl Clone for ConstPool {\n    fn clone(&self) -> Self {\n        let values = self.values.iter().map(Value::clone_immediate).collect();\n        Self { values }\n    }\n}\n\nimpl ConstPool {\n    /// Creates a constant pool from a vector of values.\n    #[must_use]\n    pub fn from_vec(values: Vec<Value>) -> Self {\n        Self { values }\n    }\n\n    /// Returns the constant at the given index.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the index is out of bounds. This should never happen with\n    /// valid bytecode since indices come from the compiler.\n    #[must_use]\n    pub fn get(&self, index: u16) -> &Value {\n        &self.values[index as usize]\n    }\n}\n\n/// Source location for a bytecode instruction, used for tracebacks.\n///\n/// Python 3.11+ tracebacks show carets under the relevant expression:\n/// ```text\n///    File \"test.py\", line 2, in foo\n///      return a + b + c\n///             ~~^~~\n/// ```\n///\n/// The `range` covers the full expression (`a + b`), while `focus` points\n/// to the specific operator (`+`) that caused the error.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct LocationEntry {\n    /// Bytecode offset this entry applies to.\n    ///\n    /// The entry applies from this offset until the next entry's offset\n    /// (or end of bytecode).\n    bytecode_offset: u32,\n\n    /// Full source range of the expression (for the underline).\n    range: CodeRange,\n\n    /// Optional focus point within the range (for the ^ caret).\n    ///\n    /// If None, the entire range is underlined without a focus caret.\n    /// This can be populated later for Python 3.11-style focused tracebacks.\n    focus: Option<CodeRange>,\n}\n\nimpl LocationEntry {\n    /// Creates a new location entry.\n    #[must_use]\n    pub fn new(bytecode_offset: u32, range: CodeRange, focus: Option<CodeRange>) -> Self {\n        Self {\n            bytecode_offset,\n            range,\n            focus,\n        }\n    }\n\n    /// Returns the full source range.\n    #[must_use]\n    pub fn range(&self) -> CodeRange {\n        self.range\n    }\n}\n\n/// Entry in the exception table - maps a protected bytecode range to its handler.\n///\n/// Instead of maintaining a runtime stack of handlers (push/pop during execution),\n/// we use a static table that's consulted when an exception is raised. This is\n/// simpler and matches CPython 3.11+'s approach.\n///\n/// For nested try blocks, multiple entries may cover the same bytecode offset.\n/// Entries are ordered innermost-first, so the VM uses the first matching entry.\n///\n/// # Example\n///\n/// For `try: x = bar(); y = baz() except ValueError as e: print(e)`:\n/// ```text\n/// 0:  LOAD_GLOBAL 'bar'\n/// 4:  CALL_FUNCTION 0\n/// 8:  STORE_LOCAL 'x'\n/// ...\n/// 24: JUMP 50              # skip handler if no exception\n/// 30: <handler code>       # exception handler starts here\n/// ```\n/// Entry: `{ start: 0, end: 24, handler: 30, stack_depth: 0 }`\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub struct ExceptionEntry {\n    /// Start of protected bytecode range (inclusive).\n    start: u32,\n\n    /// End of protected bytecode range (exclusive).\n    end: u32,\n\n    /// Bytecode offset of the exception handler.\n    handler: u32,\n\n    /// Stack depth when entering the try block.\n    ///\n    /// Used to unwind the operand stack before jumping to handler.\n    /// The VM pops values until the stack reaches this depth, then\n    /// pushes the exception value.\n    stack_depth: u16,\n}\n\nimpl ExceptionEntry {\n    /// Creates a new exception table entry.\n    #[must_use]\n    pub fn new(start: u32, end: u32, handler: u32, stack_depth: u16) -> Self {\n        Self {\n            start,\n            end,\n            handler,\n            stack_depth,\n        }\n    }\n\n    /// Returns the handler bytecode offset.\n    #[must_use]\n    pub fn handler(&self) -> u32 {\n        self.handler\n    }\n\n    /// Returns the stack depth to unwind to.\n    #[must_use]\n    pub fn stack_depth(&self) -> u16 {\n        self.stack_depth\n    }\n\n    /// Returns true if the given bytecode offset is within this entry's protected range.\n    #[must_use]\n    pub fn contains(&self, offset: u32) -> bool {\n        offset >= self.start && offset < self.end\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/compiler.rs",
    "content": "//! Bytecode compiler for transforming AST to bytecode.\n//!\n//! The compiler traverses the prepared AST (`PreparedNode` and `Expr` types from `expressions.rs`)\n//! and emits bytecode instructions using `CodeBuilder`. It handles variable scoping,\n//! control flow, and expression evaluation order following Python semantics.\n//!\n//! Functions are compiled recursively: when a `PreparedFunctionDef` is encountered,\n//! its body is compiled to bytecode and a `Function` struct is created. All compiled\n//! functions are collected and returned along with the module code.\n\nuse std::borrow::Cow;\n\nuse super::{\n    builder::{CodeBuilder, JumpLabel},\n    code::{Code, ExceptionEntry},\n    op::Opcode,\n};\nuse crate::{\n    args::{ArgExprs, CallArg, CallKwarg, Kwarg},\n    builtins::Builtins,\n    exception_private::ExcType,\n    exception_public::{MontyException, StackFrame},\n    expressions::{\n        Callable, CmpOperator, Comprehension, DictItem, Expr, ExprLoc, Identifier, Literal, NameScope, Node, Operator,\n        PreparedFunctionDef, PreparedNode, SequenceItem, UnpackTarget,\n    },\n    fstring::{ConversionFlag, FStringPart, FormatSpec, ParsedFormatSpec, encode_format_spec},\n    function::Function,\n    intern::{Interns, StringId},\n    modules::BuiltinModule,\n    parse::{CodeRange, ExceptHandler, Try},\n    value::{EitherStr, Value},\n};\n\n/// Maximum number of arguments allowed in a function call.\n///\n/// This limit comes from the bytecode format: `CallFunction` and `CallAttr`\n/// use a u8 operand for the argument count, so max 255. Python itself has no\n/// such limit but we need one for our bytecode encoding.\nconst MAX_CALL_ARGS: usize = 255;\n\n/// Compiles prepared AST nodes to bytecode.\n///\n/// The compiler traverses the AST and emits bytecode instructions using\n/// `CodeBuilder`. It handles variable scoping, control flow, and expression\n/// evaluation order following Python semantics.\n///\n/// Functions are compiled recursively and collected in the `functions` vector.\n/// When a `PreparedFunctionDef` is encountered, its body is compiled first,\n/// creating a `Function` struct that is added to the vector. The index of the\n/// function in this vector becomes the operand for MakeFunction/MakeClosure opcodes.\npub struct Compiler<'a> {\n    /// Current code being built.\n    code: CodeBuilder,\n\n    /// Reference to interns for string/function lookups.\n    interns: &'a Interns,\n\n    /// Compiled functions, indexed by their position in this vector.\n    ///\n    /// Functions are added in the order they are encountered during compilation.\n    /// Nested functions are compiled before their containing function's code\n    /// finishes, so inner functions have lower indices.\n    functions: Vec<Function>,\n\n    /// Loop stack for break/continue handling.\n    /// Each entry tracks the loop start offset and pending break jumps.\n    loop_stack: Vec<LoopInfo>,\n\n    /// Stack of finally targets for handling returns inside try-finally.\n    ///\n    /// When a return statement is compiled inside a try-finally block, instead\n    /// of immediately returning, we store the return value and jump to the\n    /// finally block. The finally block will then execute the return.\n    finally_targets: Vec<FinallyTarget>,\n\n    /// Tracks nesting depth inside exception handlers.\n    ///\n    /// When break/continue/return is inside an except handler, we need to\n    /// clear the current exception (`ClearException`) and pop the exception\n    /// value from the stack before jumping to the finally path or loop target.\n    except_handler_depth: usize,\n\n    /// Whether the compiler is currently compiling module-level code.\n    ///\n    /// At module level, `Local` and `LocalUnassigned` scopes map to global opcodes\n    /// (`LoadGlobal`/`StoreGlobal`/`DeleteGlobal`) because module locals live in the\n    /// globals array. In function bodies this is `false` and these scopes use local\n    /// opcodes that index into the stack.\n    is_module_scope: bool,\n}\n\n/// Information about a loop for break/continue handling.\n///\n/// Tracks the bytecode locations needed for compiling break and continue statements:\n/// - `start`: where continue should jump to (the ForIter instruction for `for` loops,\n///   or condition evaluation for `while` loops)\n/// - `break_jumps`: pending jumps from break statements that need to be patched\n///   to jump past the loop's else block\n/// - `has_iterator_on_stack`: whether this loop has an iterator on the stack that\n///   needs to be popped on break (true for `for` loops, false for `while` loops)\nstruct LoopInfo {\n    /// Bytecode offset of loop start (for continue).\n    start: usize,\n    /// Jump labels that need patching to loop end (for break).\n    break_jumps: Vec<JumpLabel>,\n    /// Whether this loop has an iterator on the stack.\n    /// True for `for` loops, false for `while` loops.\n    has_iterator_on_stack: bool,\n}\n\n/// A break or continue that needs to go through a finally block.\n///\n/// When break/continue is inside a try-finally, we need to run the finally block\n/// before executing the break/continue. This struct tracks the jump and which\n/// loop it targets.\nstruct BreakContinueThruFinally {\n    /// The jump instruction that needs to be patched.\n    jump: JumpLabel,\n    /// The loop depth (index in loop_stack) being targeted.\n    target_loop_depth: usize,\n}\n\n/// Tracks a finally block for handling returns/break/continue inside try-finally.\n///\n/// When compiling a try-finally, we push a `FinallyTarget` to track jumps\n/// from return/break/continue statements that need to go through the finally block.\nstruct FinallyTarget {\n    /// Jump labels for returns inside the try block that need to go to finally.\n    return_jumps: Vec<JumpLabel>,\n    /// Break statements that need to go through this finally block.\n    break_jumps: Vec<BreakContinueThruFinally>,\n    /// Continue statements that need to go through this finally block.\n    continue_jumps: Vec<BreakContinueThruFinally>,\n    /// The loop depth when this finally was entered.\n    /// Used to determine if break/continue targets a loop outside this finally.\n    loop_depth_at_entry: usize,\n}\n\n/// Result of module compilation: the module code and all compiled functions.\npub struct CompileResult {\n    /// The compiled module code.\n    pub code: Code,\n    /// All functions compiled during module compilation, indexed by their function ID.\n    pub functions: Vec<Function>,\n}\n\nimpl<'a> Compiler<'a> {\n    /// Creates a new compiler with access to the string interner.\n    fn new(interns: &'a Interns, functions: Vec<Function>) -> Self {\n        Self {\n            code: CodeBuilder::new(),\n            interns,\n            functions,\n            loop_stack: Vec::new(),\n            finally_targets: Vec::new(),\n            except_handler_depth: 0,\n            is_module_scope: false,\n        }\n    }\n\n    /// Compiles module-level code (a sequence of statements).\n    ///\n    /// Returns the compiled module Code and all compiled Functions, or a compile\n    /// error if limits were exceeded. The module implicitly returns the value\n    /// of the last expression, or None if empty.\n    pub fn compile_module(\n        nodes: &[PreparedNode],\n        interns: &Interns,\n        num_locals: u16,\n    ) -> Result<CompileResult, CompileError> {\n        Self::compile_module_with_functions(nodes, interns, num_locals, Vec::new())\n    }\n\n    /// Compiles module-level code while preserving an existing function table prefix.\n    ///\n    /// This is used by incremental REPL compilation so previously created\n    /// `FunctionId`s remain stable: new function IDs are allocated after\n    /// `existing_functions.len()`.\n    pub fn compile_module_with_functions(\n        nodes: &[PreparedNode],\n        interns: &Interns,\n        num_locals: u16,\n        existing_functions: Vec<Function>,\n    ) -> Result<CompileResult, CompileError> {\n        let mut compiler = Compiler::new(interns, Vec::new());\n        compiler.functions = existing_functions;\n        compiler.is_module_scope = true;\n        compiler.compile_block(nodes)?;\n\n        // Module returns None if no explicit return\n        compiler.code.emit(Opcode::LoadNone);\n        compiler.code.emit(Opcode::ReturnValue);\n\n        Ok(CompileResult {\n            code: compiler.code.build(num_locals),\n            functions: compiler.functions,\n        })\n    }\n\n    /// Compiles a function body to bytecode, returning the Code and any nested functions.\n    ///\n    /// Used internally when compiling function definitions. The function body is\n    /// compiled to bytecode with an implicit `return None` at the end if there's\n    /// no explicit return statement.\n    ///\n    /// The `functions` parameter receives any previously compiled functions, and\n    /// any nested functions found in the body will be added to it.\n    fn compile_function_body(\n        body: &[PreparedNode],\n        interns: &Interns,\n        functions: Vec<Function>,\n        num_locals: u16,\n    ) -> Result<(Code, Vec<Function>), CompileError> {\n        let mut compiler = Compiler::new(interns, functions);\n        compiler.compile_block(body)?;\n\n        // Implicit return None if no explicit return\n        compiler.code.emit(Opcode::LoadNone);\n        compiler.code.emit(Opcode::ReturnValue);\n\n        Ok((compiler.code.build(num_locals), compiler.functions))\n    }\n\n    /// Compiles a block of statements.\n    fn compile_block(&mut self, nodes: &[PreparedNode]) -> Result<(), CompileError> {\n        for node in nodes {\n            self.compile_stmt(node)?;\n        }\n        Ok(())\n    }\n\n    // ========================================================================\n    // Statement Compilation\n    // ========================================================================\n\n    /// Compiles a single statement.\n    fn compile_stmt(&mut self, node: &PreparedNode) -> Result<(), CompileError> {\n        // Node is an alias, use qualified path for matching\n        match node {\n            Node::Expr(expr) => {\n                self.compile_expr(expr)?;\n                self.code.emit(Opcode::Pop); // Discard result\n            }\n            Node::Return(expr) => {\n                self.compile_expr(expr)?;\n                self.compile_return();\n            }\n            Node::ReturnNone => {\n                self.code.emit(Opcode::LoadNone);\n                self.compile_return();\n            }\n            Node::Assign { target, object } => {\n                self.compile_expr(object)?;\n                self.compile_store(target);\n            }\n            Node::UnpackAssign {\n                targets,\n                targets_position,\n                object,\n            } => {\n                self.compile_expr(object)?;\n\n                // Check if there's a starred target\n                let star_idx = targets.iter().position(|t| matches!(t, UnpackTarget::Starred(_)));\n\n                // Set location to targets for proper caret in tracebacks\n                self.code.set_location(*targets_position, None);\n\n                if let Some(star_idx) = star_idx {\n                    // Has starred target - use UnpackEx\n                    let before = u8::try_from(star_idx).expect(\"too many targets before star\");\n                    let after = u8::try_from(targets.len() - star_idx - 1).expect(\"too many targets after star\");\n                    self.code.emit_u8_u8(Opcode::UnpackEx, before, after);\n                } else {\n                    // No starred target - use UnpackSequence\n                    let count = u8::try_from(targets.len()).expect(\"too many targets in unpack\");\n                    self.code.emit_u8(Opcode::UnpackSequence, count);\n                }\n\n                // After UnpackSequence/UnpackEx, values are on stack with first item on top\n                // Store them in order (first target gets first item), handling nesting\n                for target in targets {\n                    self.compile_unpack_target(target);\n                }\n            }\n            Node::OpAssign { target, op, object } => {\n                let Some(opcode) = operator_to_inplace_opcode(op) else {\n                    return Err(CompileError::new(\n                        \"matrix multiplication augmented assignment (@=) is not yet supported\",\n                        target.position,\n                    ));\n                };\n                self.compile_name(target);\n                self.compile_expr(object)?;\n                self.code.emit(opcode);\n                self.compile_store(target);\n            }\n            Node::SubscriptOpAssign {\n                target,\n                index,\n                op,\n                object,\n                target_position,\n            } => {\n                let Some(opcode) = operator_to_inplace_opcode(op) else {\n                    return Err(CompileError::new(\n                        \"matrix multiplication augmented assignment (@=) is not yet supported\",\n                        *target_position,\n                    ));\n                };\n                self.compile_name(target);\n                self.compile_expr(index)?;\n                self.code.emit(Opcode::Dup2);\n                self.code.set_location(*target_position, None);\n                self.code.emit(Opcode::BinarySubscr);\n                self.compile_expr(object)?;\n                self.code.emit(opcode);\n                self.code.emit(Opcode::Rot3);\n                self.code.set_location(*target_position, None);\n                self.code.emit(Opcode::StoreSubscr);\n            }\n            Node::SubscriptAssign {\n                target,\n                index,\n                value,\n                target_position,\n            } => {\n                // Stack order for StoreSubscr: value, obj, index\n                self.compile_expr(value)?;\n                self.compile_name(target);\n                self.compile_expr(index)?;\n                // Set location to the target (e.g., `lst[10]`) for proper caret in tracebacks\n                self.code.set_location(*target_position, None);\n                self.code.emit(Opcode::StoreSubscr);\n            }\n            Node::AttrAssign {\n                object,\n                attr,\n                target_position,\n                value,\n            } => {\n                // Stack order for StoreAttr: value, obj\n                self.compile_expr(value)?;\n                self.compile_expr(object)?;\n                let name_id = attr.string_id().expect(\"StoreAttr requires interned attr name\");\n                // Set location to the target (e.g., `x.foo`) for proper caret in tracebacks\n                self.code.set_location(*target_position, None);\n                self.code.emit_u16(\n                    Opcode::StoreAttr,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                );\n            }\n            Node::If { test, body, or_else } => self.compile_if(test, body, or_else)?,\n            Node::For {\n                target,\n                iter,\n                body,\n                or_else,\n            } => self.compile_for(target, iter, body, or_else)?,\n            Node::While { test, body, or_else } => self.compile_while(test, body, or_else)?,\n            Node::Assert { test, msg } => self.compile_assert(test, msg.as_ref())?,\n            Node::Raise(expr) => {\n                if let Some(exc) = expr {\n                    self.compile_expr(exc)?;\n                    self.code.emit(Opcode::Raise);\n                } else {\n                    self.code.emit(Opcode::Reraise);\n                }\n            }\n            Node::FunctionDef(func_def) => self.compile_function_def(func_def)?,\n            Node::Try(try_block) => self.compile_try(try_block)?,\n            Node::Import { module_name, binding } => self.compile_import(*module_name, binding),\n            Node::ImportFrom {\n                module_name,\n                names,\n                position,\n            } => self.compile_import_from(*module_name, names, *position),\n            Node::Break { position } => self.compile_break(*position)?,\n            Node::Continue { position } => self.compile_continue(*position)?,\n            // These are handled during the prepare phase and produce no bytecode\n            Node::Pass | Node::Global { .. } | Node::Nonlocal { .. } => {}\n        }\n        Ok(())\n    }\n\n    /// Compiles a function definition.\n    ///\n    /// This involves:\n    /// 1. Recursively compiling the function body to bytecode\n    /// 2. Creating a Function struct with the compiled Code\n    /// 3. Adding the Function to the compiler's functions vector\n    /// 4. Emitting bytecode to evaluate defaults and create the function at runtime\n    fn compile_function_def(&mut self, func_def: &PreparedFunctionDef) -> Result<(), CompileError> {\n        let func_pos = func_def.name.position;\n\n        // Check bytecode operand limits\n        if func_def.default_exprs.len() > MAX_CALL_ARGS {\n            return Err(CompileError::new(\n                format!(\"more than {MAX_CALL_ARGS} default parameter values\"),\n                func_pos,\n            ));\n        }\n        if func_def.free_var_enclosing_slots.len() > MAX_CALL_ARGS {\n            return Err(CompileError::new(\n                format!(\"more than {MAX_CALL_ARGS} closure variables\"),\n                func_pos,\n            ));\n        }\n\n        // 1. Compile the function body recursively\n        // Take ownership of functions for the recursive compile, then restore\n        let functions = std::mem::take(&mut self.functions);\n        let namespace_size = u16::try_from(func_def.namespace_size).expect(\"function namespace size exceeds u16\");\n        let (body_code, mut functions) =\n            Self::compile_function_body(&func_def.body, self.interns, functions, namespace_size)?;\n\n        // 2. Create the compiled Function and add to the vector\n        let func_id = functions.len();\n        let function = Function::new(\n            func_def.name,\n            func_def.signature.clone(),\n            func_def.namespace_size,\n            func_def.free_var_enclosing_slots.clone(),\n            func_def.cell_var_count,\n            func_def.cell_param_indices.clone(),\n            func_def.default_exprs.len(),\n            func_def.is_async,\n            body_code,\n        );\n        functions.push(function);\n\n        // Restore functions to self\n        self.functions = functions;\n\n        // 3. Compile and push default values (evaluated at definition time)\n        for default_expr in &func_def.default_exprs {\n            self.compile_expr(default_expr)?;\n        }\n        let defaults_count =\n            u8::try_from(func_def.default_exprs.len()).expect(\"function default argument count exceeds u8\");\n        let func_id_u16 = u16::try_from(func_id).expect(\"function count exceeds u16\");\n\n        // 4. Emit MakeFunction or MakeClosure (if has free vars)\n        if func_def.free_var_enclosing_slots.is_empty() {\n            // MakeFunction: func_id (u16) + defaults_count (u8)\n            self.code.emit_u16_u8(Opcode::MakeFunction, func_id_u16, defaults_count);\n        } else {\n            // Push captured cells from enclosing scope\n            for &slot in &func_def.free_var_enclosing_slots {\n                // Load the cell reference from the enclosing namespace\n                let slot_u16 = u16::try_from(slot.index()).expect(\"closure slot index exceeds u16\");\n                self.code.emit_load_local(slot_u16);\n            }\n            let cell_count =\n                u8::try_from(func_def.free_var_enclosing_slots.len()).expect(\"closure cell count exceeds u8\");\n            // MakeClosure: func_id (u16) + defaults_count (u8) + cell_count (u8)\n            self.code\n                .emit_u16_u8_u8(Opcode::MakeClosure, func_id_u16, defaults_count, cell_count);\n        }\n\n        // 5. Store the function object to its name slot\n        self.compile_store(&func_def.name);\n\n        Ok(())\n    }\n\n    /// Compiles a lambda expression.\n    ///\n    /// This is similar to `compile_function_def` but:\n    /// - Does NOT store the function to a name slot (it stays on the stack as an expression result)\n    ///\n    /// The lambda's `PreparedFunctionDef` already has `<lambda>` as its name.\n    fn compile_lambda(&mut self, func_def: &PreparedFunctionDef) -> Result<(), CompileError> {\n        let func_pos = func_def.name.position;\n\n        // Check bytecode operand limits\n        if func_def.default_exprs.len() > MAX_CALL_ARGS {\n            return Err(CompileError::new(\n                format!(\"more than {MAX_CALL_ARGS} default parameter values\"),\n                func_pos,\n            ));\n        }\n        if func_def.free_var_enclosing_slots.len() > MAX_CALL_ARGS {\n            return Err(CompileError::new(\n                format!(\"more than {MAX_CALL_ARGS} closure variables\"),\n                func_pos,\n            ));\n        }\n\n        // 1. Compile the function body recursively\n        let functions = std::mem::take(&mut self.functions);\n        let namespace_size = u16::try_from(func_def.namespace_size).expect(\"function namespace size exceeds u16\");\n        let (body_code, mut functions) =\n            Self::compile_function_body(&func_def.body, self.interns, functions, namespace_size)?;\n\n        // 2. Create the compiled Function and add to the vector\n        let func_id = functions.len();\n        let function = Function::new(\n            func_def.name,\n            func_def.signature.clone(),\n            func_def.namespace_size,\n            func_def.free_var_enclosing_slots.clone(),\n            func_def.cell_var_count,\n            func_def.cell_param_indices.clone(),\n            func_def.default_exprs.len(),\n            func_def.is_async,\n            body_code,\n        );\n        functions.push(function);\n\n        // Restore functions to self\n        self.functions = functions;\n\n        // 3. Compile and push default values (evaluated at definition time)\n        for default_expr in &func_def.default_exprs {\n            self.compile_expr(default_expr)?;\n        }\n        let defaults_count =\n            u8::try_from(func_def.default_exprs.len()).expect(\"function default argument count exceeds u8\");\n        let func_id_u16 = u16::try_from(func_id).expect(\"function count exceeds u16\");\n\n        // 4. Emit MakeFunction or MakeClosure (if has free vars)\n        if func_def.free_var_enclosing_slots.is_empty() {\n            // MakeFunction: func_id (u16) + defaults_count (u8)\n            self.code.emit_u16_u8(Opcode::MakeFunction, func_id_u16, defaults_count);\n        } else {\n            // Push captured cells from enclosing scope\n            for &slot in &func_def.free_var_enclosing_slots {\n                let slot_u16 = u16::try_from(slot.index()).expect(\"closure slot index exceeds u16\");\n                self.code.emit_load_local(slot_u16);\n            }\n            let cell_count =\n                u8::try_from(func_def.free_var_enclosing_slots.len()).expect(\"closure cell count exceeds u8\");\n            // MakeClosure: func_id (u16) + defaults_count (u8) + cell_count (u8)\n            self.code\n                .emit_u16_u8_u8(Opcode::MakeClosure, func_id_u16, defaults_count, cell_count);\n        }\n\n        // NOTE: Unlike compile_function_def, we do NOT call compile_store here.\n        // The function object stays on the stack as an expression result.\n\n        Ok(())\n    }\n\n    /// Compiles an import statement.\n    ///\n    /// Emits `LoadModule` to create the module, then stores it to the binding name.\n    /// If the module is unknown, emits `RaiseImportError` to defer the error to runtime.\n    /// This allows imports inside `if TYPE_CHECKING:` blocks to compile successfully.\n    fn compile_import(&mut self, module_name: StringId, binding: &Identifier) {\n        let position = binding.position;\n        self.code.set_location(position, None);\n\n        // Look up the module by name\n        if let Some(builtin_module) = BuiltinModule::from_string_id(module_name) {\n            // Known module - emit LoadModule\n            self.code.emit_u8(Opcode::LoadModule, builtin_module as u8);\n            // Store to the binding (respects Local/Global/Cell scope)\n            self.compile_store(binding);\n        } else {\n            // Unknown module - defer error to runtime with RaiseImportError\n            // This allows TYPE_CHECKING imports to compile without error\n            let name_const = self.code.add_const(Value::InternString(module_name));\n            self.code.emit_u16(Opcode::RaiseImportError, name_const);\n        }\n    }\n\n    /// Compiles a `from module import name, ...` statement.\n    ///\n    /// Creates the module once, then loads each attribute and stores to the binding.\n    /// Invalid attribute names will raise `AttributeError` at runtime.\n    /// If the module is unknown, emits `RaiseImportError` to defer the error to runtime.\n    /// This allows imports inside `if TYPE_CHECKING:` blocks to compile successfully.\n    fn compile_import_from(&mut self, module_name: StringId, names: &[(StringId, Identifier)], position: CodeRange) {\n        self.code.set_location(position, None);\n\n        // Look up the module\n        if let Some(builtin_module) = BuiltinModule::from_string_id(module_name) {\n            // Known module - emit LoadModule\n            self.code.emit_u8(Opcode::LoadModule, builtin_module as u8);\n\n            // For each name to import\n            for (i, (import_name, binding)) in names.iter().enumerate() {\n                // Dup the module if this isn't the last import (last one consumes the module)\n                if i < names.len() - 1 {\n                    self.code.emit(Opcode::Dup);\n                }\n\n                // Load the attribute from the module (raises ImportError if not found)\n                let name_idx = u16::try_from(import_name.index()).expect(\"name index exceeds u16\");\n                self.code.emit_u16(Opcode::LoadAttrImport, name_idx);\n\n                // Store to the binding\n                self.compile_store(binding);\n            }\n        } else {\n            // Unknown module - defer error to runtime with RaiseImportError\n            // This allows TYPE_CHECKING imports to compile without error\n            let name_const = self.code.add_const(Value::InternString(module_name));\n            self.code.emit_u16(Opcode::RaiseImportError, name_const);\n        }\n    }\n\n    // ========================================================================\n    // Expression Compilation\n    // ========================================================================\n\n    /// Compiles an expression, leaving its value on the stack.\n    fn compile_expr(&mut self, expr_loc: &ExprLoc) -> Result<(), CompileError> {\n        // Set source location for traceback info\n        self.code.set_location(expr_loc.position, None);\n\n        match &expr_loc.expr {\n            Expr::Literal(lit) => self.compile_literal(lit),\n\n            Expr::Name(ident) => self.compile_name(ident),\n\n            Expr::Builtin(builtin) => {\n                let idx = self.code.add_const(Value::Builtin(*builtin));\n                self.code.emit_u16(Opcode::LoadConst, idx);\n            }\n\n            Expr::Op { left, op, right } => {\n                self.compile_binary_op(left, op, right, expr_loc.position)?;\n            }\n\n            Expr::CmpOp { left, op, right } => {\n                self.compile_expr(left)?;\n                self.compile_expr(right)?;\n                // Restore the full comparison expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                // ModEq needs special handling - it has a constant operand\n                if let CmpOperator::ModEq(value) = op {\n                    let const_idx = self.code.add_const(Value::Int(*value));\n                    self.code.emit_u16(Opcode::CompareModEq, const_idx);\n                } else {\n                    self.code.emit(cmp_operator_to_opcode(op));\n                }\n            }\n\n            Expr::ChainCmp { left, comparisons } => {\n                self.compile_chain_comparison(left, comparisons, expr_loc.position)?;\n            }\n\n            Expr::Not(operand) => {\n                self.compile_expr(operand)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                self.code.emit(Opcode::UnaryNot);\n            }\n\n            Expr::UnaryMinus(operand) => {\n                self.compile_expr(operand)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                self.code.emit(Opcode::UnaryNeg);\n            }\n\n            Expr::UnaryPlus(operand) => {\n                self.compile_expr(operand)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                self.code.emit(Opcode::UnaryPos);\n            }\n\n            Expr::UnaryInvert(operand) => {\n                self.compile_expr(operand)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                self.code.emit(Opcode::UnaryInvert);\n            }\n\n            Expr::List(elements) => {\n                if has_unpack_seq(elements) {\n                    // Generalized path: build incrementally for PEP 448 *unpacks\n                    self.code.emit_u16(Opcode::BuildList, 0);\n                    for item in elements {\n                        match item {\n                            SequenceItem::Value(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit_u8(Opcode::ListAppend, 0);\n                            }\n                            SequenceItem::Unpack(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit(Opcode::ListExtend);\n                            }\n                        }\n                    }\n                } else {\n                    // Fast path: all values, single BuildList.\n                    // SAFETY: has_unpack_seq(elements) is false, so every item is Value.\n                    for item in elements {\n                        let SequenceItem::Value(e) = item else {\n                            unreachable!(\"list fast path: only Value items\")\n                        };\n                        self.compile_expr(e)?;\n                    }\n                    self.code.emit_u16(\n                        Opcode::BuildList,\n                        u16::try_from(elements.len()).expect(\"elements count exceeds u16\"),\n                    );\n                }\n            }\n\n            Expr::Tuple(elements) => {\n                if has_unpack_seq(elements) {\n                    // Generalized path: build via list then convert for PEP 448 *unpacks\n                    self.code.emit_u16(Opcode::BuildList, 0);\n                    for item in elements {\n                        match item {\n                            SequenceItem::Value(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit_u8(Opcode::ListAppend, 0);\n                            }\n                            SequenceItem::Unpack(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit(Opcode::ListExtend);\n                            }\n                        }\n                    }\n                    self.code.emit(Opcode::ListToTuple);\n                } else {\n                    // Fast path: all values, single BuildTuple.\n                    // SAFETY: has_unpack_seq(elements) is false, so every item is Value.\n                    for item in elements {\n                        let SequenceItem::Value(e) = item else {\n                            unreachable!(\"tuple fast path: only Value items\")\n                        };\n                        self.compile_expr(e)?;\n                    }\n                    self.code.emit_u16(\n                        Opcode::BuildTuple,\n                        u16::try_from(elements.len()).expect(\"elements count exceeds u16\"),\n                    );\n                }\n            }\n\n            Expr::Dict(dict_items) => {\n                if has_unpack_dict(dict_items) {\n                    // Generalized path: build incrementally for PEP 448 **unpacks\n                    self.code.emit_u16(Opcode::BuildDict, 0);\n                    for item in dict_items {\n                        match item {\n                            DictItem::Pair(key, value) => {\n                                self.compile_expr(key)?;\n                                self.compile_expr(value)?;\n                                // depth=0: dict is at TOS after key/value are popped\n                                self.code.emit_u8(Opcode::DictSetItem, 0);\n                            }\n                            DictItem::Unpack(e) => {\n                                self.compile_expr(e)?;\n                                // depth=0: dict is directly below mapping on stack\n                                self.code.emit_u8(Opcode::DictUpdate, 0);\n                            }\n                        }\n                    }\n                } else {\n                    // Fast path: all pairs, single BuildDict.\n                    // SAFETY: has_unpack_dict(dict_items) is false, so every item is Pair.\n                    for item in dict_items {\n                        let DictItem::Pair(key, value) = item else {\n                            unreachable!(\"dict fast path: only Pair items\")\n                        };\n                        self.compile_expr(key)?;\n                        self.compile_expr(value)?;\n                    }\n                    self.code.emit_u16(\n                        Opcode::BuildDict,\n                        u16::try_from(dict_items.len()).expect(\"pairs count exceeds u16\"),\n                    );\n                }\n            }\n\n            Expr::Set(elements) => {\n                if has_unpack_seq(elements) {\n                    // Generalized path: build incrementally for PEP 448 *unpacks\n                    self.code.emit_u16(Opcode::BuildSet, 0);\n                    for item in elements {\n                        match item {\n                            SequenceItem::Value(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit_u8(Opcode::SetAdd, 0);\n                            }\n                            SequenceItem::Unpack(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit_u8(Opcode::SetExtend, 0);\n                            }\n                        }\n                    }\n                } else {\n                    // Fast path: all values, single BuildSet.\n                    // SAFETY: has_unpack_seq(elements) is false, so every item is Value.\n                    for item in elements {\n                        let SequenceItem::Value(e) = item else {\n                            unreachable!(\"set fast path: only Value items\")\n                        };\n                        self.compile_expr(e)?;\n                    }\n                    self.code.emit_u16(\n                        Opcode::BuildSet,\n                        u16::try_from(elements.len()).expect(\"elements count exceeds u16\"),\n                    );\n                }\n            }\n\n            Expr::Subscript { object, index } => {\n                self.compile_expr(object)?;\n                self.compile_expr(index)?;\n                // Restore the full subscript expression's position for traceback\n                self.code.set_location(expr_loc.position, None);\n                self.code.emit(Opcode::BinarySubscr);\n            }\n\n            Expr::IfElse { test, body, orelse } => {\n                self.compile_if_else_expr(test, body, orelse)?;\n            }\n\n            Expr::AttrGet { object, attr } => {\n                self.compile_expr(object)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                let name_id = attr.string_id().expect(\"LoadAttr requires interned attr name\");\n                self.code.emit_u16(\n                    Opcode::LoadAttr,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                );\n            }\n\n            Expr::Call { callable, args } => {\n                self.compile_call(callable, args, expr_loc.position)?;\n            }\n\n            Expr::AttrCall { object, attr, args } => {\n                // Compile the object (will be on the stack)\n                self.compile_expr(object)?;\n\n                // Compile the attribute call arguments and emit CallAttr\n                self.compile_method_call(attr, args, expr_loc.position)?;\n            }\n\n            Expr::IndirectCall { callable, args } => {\n                // Compile the callable expression (e.g., a lambda)\n                self.compile_expr(callable)?;\n\n                // Compile arguments and emit the call\n                self.compile_call_args(args, expr_loc.position)?;\n            }\n\n            Expr::FString(parts) => {\n                // Compile each part and build the f-string\n                let part_count = self.compile_fstring_parts(parts)?;\n                self.code.emit_u16(Opcode::BuildFString, part_count);\n            }\n\n            Expr::ListComp { elt, generators } => {\n                self.compile_list_comp(elt, generators)?;\n            }\n\n            Expr::SetComp { elt, generators } => {\n                self.compile_set_comp(elt, generators)?;\n            }\n\n            Expr::DictComp { key, value, generators } => {\n                self.compile_dict_comp(key, value, generators)?;\n            }\n\n            Expr::Lambda { func_def } => {\n                self.compile_lambda(func_def)?;\n            }\n\n            Expr::LambdaRaw { .. } => {\n                // LambdaRaw should be converted to Lambda during prepare phase\n                unreachable!(\"Expr::LambdaRaw should not exist after prepare phase\")\n            }\n\n            Expr::Await(value) => {\n                // Await expressions: compile the inner expression, then emit Await\n                // Await handles ExternalFuture, Coroutine, and GatherFuture\n                self.compile_expr(value)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(expr_loc.position, None);\n                self.code.emit(Opcode::Await);\n            }\n\n            Expr::Slice { lower, upper, step } => {\n                // Compile slice components: start, stop, step (push None for missing)\n                if let Some(lower) = lower {\n                    self.compile_expr(lower)?;\n                } else {\n                    self.code.emit(Opcode::LoadNone);\n                }\n                if let Some(upper) = upper {\n                    self.compile_expr(upper)?;\n                } else {\n                    self.code.emit(Opcode::LoadNone);\n                }\n                if let Some(step) = step {\n                    self.compile_expr(step)?;\n                } else {\n                    self.code.emit(Opcode::LoadNone);\n                }\n                self.code.emit(Opcode::BuildSlice);\n            }\n\n            Expr::Named { target, value } => {\n                // Compile the value expression (leaves result on stack)\n                self.compile_expr(value)?;\n                // Duplicate so value remains after store\n                self.code.emit(Opcode::Dup);\n                // Store to target (pops one copy)\n                self.compile_store(target);\n            }\n        }\n        Ok(())\n    }\n\n    // ========================================================================\n    // Literal Compilation\n    // ========================================================================\n\n    /// Compiles a literal value.\n    fn compile_literal(&mut self, literal: &Literal) {\n        match literal {\n            Literal::None => {\n                self.code.emit(Opcode::LoadNone);\n            }\n\n            Literal::Bool(true) => {\n                self.code.emit(Opcode::LoadTrue);\n            }\n\n            Literal::Bool(false) => {\n                self.code.emit(Opcode::LoadFalse);\n            }\n\n            Literal::Int(n) => {\n                // Use LoadSmallInt for values that fit in i8\n                if let Ok(small) = i8::try_from(*n) {\n                    self.code.emit_i8(Opcode::LoadSmallInt, small);\n                } else {\n                    let idx = self.code.add_const(Value::from(*literal));\n                    self.code.emit_u16(Opcode::LoadConst, idx);\n                }\n            }\n\n            // For Float, Str, Bytes, Ellipsis - use LoadConst with Value::from\n            _ => {\n                let idx = self.code.add_const(Value::from(*literal));\n                self.code.emit_u16(Opcode::LoadConst, idx);\n            }\n        }\n    }\n\n    // ========================================================================\n    // Variable Operations\n    // ========================================================================\n\n    /// Compiles loading a variable onto the stack.\n    ///\n    /// At module level, `Local` and `LocalUnassigned` scopes emit global opcodes\n    /// because module-level locals live in the globals array.\n    fn compile_name(&mut self, ident: &Identifier) {\n        let slot = u16::try_from(ident.namespace_id().index()).expect(\"local slot exceeds u16\");\n        match ident.scope {\n            NameScope::Local => {\n                // True local - register name and mark as assigned for UnboundLocalError\n                self.code.register_local_name(slot, ident.name_id);\n                self.code.register_assigned_local(slot);\n                if self.is_module_scope {\n                    self.code.emit_u16(Opcode::LoadGlobal, slot);\n                } else {\n                    self.code.emit_load_local(slot);\n                }\n            }\n            NameScope::LocalUnassigned => {\n                // Undefined reference - register name but NOT as assigned for NameError\n                self.code.register_local_name(slot, ident.name_id);\n                if self.is_module_scope {\n                    self.code.emit_u16(Opcode::LoadGlobal, slot);\n                } else {\n                    self.code.emit_load_local(slot);\n                }\n            }\n            NameScope::Global => {\n                // Register the name for NameError/NameLookup messages\n                self.code.register_local_name(slot, ident.name_id);\n                self.code.emit_u16(Opcode::LoadGlobal, slot);\n            }\n            NameScope::Cell => {\n                // Register the name for NameError messages (unbound free variable)\n                self.code.register_local_name(slot, ident.name_id);\n                // Emit local slot index — the VM reads the cell HeapId from the stack\n                self.code.emit_u16(Opcode::LoadCell, slot);\n            }\n        }\n    }\n\n    /// Compiles loading a variable in call context (e.g., `foo()` loads `foo`).\n    ///\n    /// For `LocalUnassigned` and `Global` scopes, emits callable-aware load opcodes\n    /// that push `ExtFunction(name_id)` for undefined names instead of yielding\n    /// `NameLookup`. This allows execution to reach `CallFunction`, which naturally\n    /// yields `FunctionCall` — giving the host a chance to handle external function calls.\n    ///\n    /// For `Local` and `Cell` scopes, delegates to `compile_name` since those can't\n    /// be external functions (they're always defined locally or captured).\n    fn compile_name_callable(&mut self, ident: &Identifier) {\n        let slot = u16::try_from(ident.namespace_id().index()).expect(\"local slot exceeds u16\");\n        match ident.scope {\n            NameScope::LocalUnassigned => {\n                // Undefined reference in call context - use callable-aware load.\n                // At module level, use global callable since locals are in the globals array.\n                self.code.register_local_name(slot, ident.name_id);\n                if self.is_module_scope {\n                    self.code.emit_load_global_callable(slot, ident.name_id);\n                } else {\n                    self.code.emit_load_local_callable(slot, ident.name_id);\n                }\n            }\n            NameScope::Global => {\n                // Global scope - name_id is encoded in the operand because global slot\n                // indices are in a different namespace from local slots, so looking up\n                // the name from the current frame's local_names would be incorrect\n                self.code.emit_load_global_callable(slot, ident.name_id);\n            }\n            // Local and Cell can't be external functions - use regular load\n            NameScope::Local | NameScope::Cell => self.compile_name(ident),\n        }\n    }\n\n    /// Compiles storing the top of stack to a variable.\n    ///\n    /// At module level, `Local` and `LocalUnassigned` scopes emit `StoreGlobal`\n    /// because module-level locals live in the globals array.\n    fn compile_store(&mut self, target: &Identifier) {\n        let slot = u16::try_from(target.namespace_id().index()).expect(\"local slot exceeds u16\");\n        match target.scope {\n            NameScope::Local | NameScope::LocalUnassigned => {\n                self.code.register_local_name(slot, target.name_id);\n                if self.is_module_scope {\n                    self.code.emit_u16(Opcode::StoreGlobal, slot);\n                } else {\n                    self.code.emit_store_local(slot);\n                }\n            }\n            NameScope::Global => {\n                self.code.emit_u16(Opcode::StoreGlobal, slot);\n            }\n            NameScope::Cell => {\n                // Emit local slot index — the VM reads the cell HeapId from the stack\n                self.code.emit_u16(Opcode::StoreCell, slot);\n            }\n        }\n    }\n\n    // ========================================================================\n    // Binary Operator Compilation\n    // ========================================================================\n\n    /// Compiles a binary operation.\n    ///\n    /// `parent_pos` is the position of the full binary expression (e.g., `1 / 0`),\n    /// which we restore before emitting the opcode so tracebacks show the right range.\n    fn compile_binary_op(\n        &mut self,\n        left: &ExprLoc,\n        op: &Operator,\n        right: &ExprLoc,\n        parent_pos: CodeRange,\n    ) -> Result<(), CompileError> {\n        match op {\n            // Short-circuit AND: evaluate left, jump if falsy\n            Operator::And => {\n                self.compile_expr(left)?;\n                let end_jump = self.code.emit_jump(Opcode::JumpIfFalseOrPop);\n                self.compile_expr(right)?;\n                self.code.patch_jump(end_jump);\n            }\n\n            // Short-circuit OR: evaluate left, jump if truthy\n            Operator::Or => {\n                self.compile_expr(left)?;\n                let end_jump = self.code.emit_jump(Opcode::JumpIfTrueOrPop);\n                self.compile_expr(right)?;\n                self.code.patch_jump(end_jump);\n            }\n\n            // Regular binary operators\n            _ => {\n                self.compile_expr(left)?;\n                self.compile_expr(right)?;\n                // Restore the full expression's position for traceback caret range\n                self.code.set_location(parent_pos, None);\n                self.code.emit(operator_to_opcode(op));\n            }\n        }\n        Ok(())\n    }\n\n    /// Compiles a chain comparison expression like `a < b < c < d`.\n    ///\n    /// Chain comparisons evaluate each intermediate value only once and short-circuit\n    /// on the first false result. Uses stack manipulation to avoid namespace pollution.\n    ///\n    /// Bytecode strategy for `a < b < c`:\n    /// ```text\n    /// eval a              # Stack: [a]\n    /// eval b              # Stack: [a, b]\n    /// Dup                 # Stack: [a, b, b]\n    /// Rot3                # Stack: [b, a, b]\n    /// CompareLt           # Stack: [b, result1]\n    /// JumpIfFalseOrPop    # if false: jump to cleanup; if true: pop, stack=[b]\n    /// eval c              # Stack: [b, c]\n    /// CompareLt           # Stack: [result2]\n    /// Jump @end\n    /// @cleanup:           # Stack: [b, False]\n    /// Rot2                # Stack: [False, b]\n    /// Pop                 # Stack: [False]\n    /// @end:\n    /// ```\n    fn compile_chain_comparison(\n        &mut self,\n        left: &ExprLoc,\n        comparisons: &[(CmpOperator, ExprLoc)],\n        position: CodeRange,\n    ) -> Result<(), CompileError> {\n        let n = comparisons.len();\n\n        // Remember stack depth before the chain for cleanup calculation\n        let base_depth = self.code.stack_depth();\n\n        // Compile leftmost operand\n        self.compile_expr(left)?;\n\n        // Track jump targets for short-circuit cleanup\n        let mut cleanup_jumps = Vec::with_capacity(n - 1);\n\n        for (i, (op, right)) in comparisons.iter().enumerate() {\n            let is_last = i == n - 1;\n\n            // Compile the right operand\n            self.compile_expr(right)?;\n\n            if !is_last {\n                // Keep a copy of the intermediate for the next comparison\n                self.code.emit(Opcode::Dup);\n                // Reorder: [prev, curr, curr] -> [curr, prev, curr]\n                self.code.emit(Opcode::Rot3);\n            }\n\n            // Emit comparison\n            self.code.set_location(position, None);\n            if let CmpOperator::ModEq(value) = op {\n                let const_idx = self.code.add_const(Value::Int(*value));\n                self.code.emit_u16(Opcode::CompareModEq, const_idx);\n            } else {\n                self.code.emit(cmp_operator_to_opcode(op));\n            }\n\n            if !is_last {\n                // Short-circuit: if false, jump to cleanup\n                let jump = self.code.emit_jump(Opcode::JumpIfFalseOrPop);\n                cleanup_jumps.push(jump);\n            }\n        }\n\n        // Jump past cleanup (result already on stack)\n        let end_jump = self.code.emit_jump(Opcode::Jump);\n\n        // Cleanup: remove the saved intermediate value, keep False result\n        // The cleanup is only reached via JumpIfFalseOrPop which doesn't pop,\n        // so the stack has: [intermediate, False] (2 extra items from base)\n        for jump in cleanup_jumps {\n            self.code.patch_jump(jump);\n        }\n        self.code.set_stack_depth(base_depth + 2); // [intermediate, False]\n        self.code.emit(Opcode::Rot2); // [False, intermediate]\n        self.code.emit(Opcode::Pop); // [False]\n\n        self.code.patch_jump(end_jump);\n        // Final result is on stack: base_depth + 1\n        self.code.set_stack_depth(base_depth + 1);\n\n        Ok(())\n    }\n\n    // ========================================================================\n    // Control Flow Compilation\n    // ========================================================================\n\n    /// Compiles an if/else statement.\n    fn compile_if(\n        &mut self,\n        test: &ExprLoc,\n        body: &[PreparedNode],\n        or_else: &[PreparedNode],\n    ) -> Result<(), CompileError> {\n        self.compile_expr(test)?;\n\n        if or_else.is_empty() {\n            // Simple if without else\n            let end_jump = self.code.emit_jump(Opcode::JumpIfFalse);\n            self.compile_block(body)?;\n            self.code.patch_jump(end_jump);\n        } else {\n            // If with else\n            let else_jump = self.code.emit_jump(Opcode::JumpIfFalse);\n            self.compile_block(body)?;\n            let end_jump = self.code.emit_jump(Opcode::Jump);\n            self.code.patch_jump(else_jump);\n            self.compile_block(or_else)?;\n            self.code.patch_jump(end_jump);\n        }\n        Ok(())\n    }\n\n    /// Compiles a ternary conditional expression.\n    fn compile_if_else_expr(&mut self, test: &ExprLoc, body: &ExprLoc, orelse: &ExprLoc) -> Result<(), CompileError> {\n        self.compile_expr(test)?;\n        let else_jump = self.code.emit_jump(Opcode::JumpIfFalse);\n        self.compile_expr(body)?;\n        let end_jump = self.code.emit_jump(Opcode::Jump);\n        self.code.patch_jump(else_jump);\n        self.compile_expr(orelse)?;\n        self.code.patch_jump(end_jump);\n        Ok(())\n    }\n\n    /// Compiles a function call expression.\n    ///\n    /// For builtin calls with positional-only arguments, emits the optimized `CallBuiltin`\n    /// opcode which avoids pushing/popping the callable on the stack.\n    ///\n    /// For other calls, pushes the callable onto the stack, then all arguments, then emits\n    /// `CallFunction` or `CallFunctionKw`.\n    ///\n    /// The `call_pos` is the position of the full call expression for proper traceback caret.\n    fn compile_call(&mut self, callable: &Callable, args: &ArgExprs, call_pos: CodeRange) -> Result<(), CompileError> {\n        // Check if we can use the optimized CallBuiltinFunction path:\n        // - Callable must be a builtin function (known at compile time)\n        // - Arguments must be positional-only (Empty, One, Two, or Args)\n        if let Callable::Builtin(Builtins::Function(builtin_func)) = callable\n            && let Some(arg_count) = self.compile_builtin_call(args, call_pos)?\n        {\n            // Optimization applied - CallBuiltinFunction emitted\n            self.code.set_location(call_pos, None);\n            self.code.emit_call_builtin_function(*builtin_func as u8, arg_count);\n            return Ok(());\n        }\n        // Fall through to standard path for kwargs/unpacking\n\n        // Check if we can use the optimized CallBuiltinType path:\n        // - Callable must be a builtin type constructor (known at compile time)\n        // - Arguments must be positional-only (Empty, One, Two, or Args)\n        if let Callable::Builtin(Builtins::Type(t)) = callable\n            && let Some(type_id) = t.callable_to_u8()\n            && let Some(arg_count) = self.compile_builtin_call(args, call_pos)?\n        {\n            // Optimization applied - CallBuiltinType emitted\n            self.code.set_location(call_pos, None);\n            self.code.emit_call_builtin_type(type_id, arg_count);\n            return Ok(());\n        }\n        // Fall through to standard path for kwargs/unpacking or non-callable types\n\n        // Standard path: push callable, compile args, emit CallFunction/CallFunctionKw\n        // Push the callable (use name position for NameError caret range)\n        match callable {\n            Callable::Builtin(builtin) => {\n                let idx = self.code.add_const(Value::Builtin(*builtin));\n                self.code.emit_u16(Opcode::LoadConst, idx);\n            }\n            Callable::Name(ident) => {\n                // Use callable-aware load opcodes so undefined names produce ExtFunction\n                // instead of yielding NameLookup, allowing CallFunction to yield FunctionCall\n                self.code.set_location(ident.position, None);\n                self.compile_name_callable(ident);\n            }\n        }\n\n        // Compile arguments and emit the call\n        // Restore full call position before CallFunction for call-related errors\n        match args {\n            ArgExprs::Empty => {\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, 0);\n            }\n            ArgExprs::One(arg) => {\n                self.compile_expr(arg)?;\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, 1);\n            }\n            ArgExprs::Two(arg1, arg2) => {\n                self.compile_expr(arg1)?;\n                self.compile_expr(arg2)?;\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, 2);\n            }\n            ArgExprs::Args(args) => {\n                // Check argument count limit before compiling\n                if args.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} positional arguments in function call\"),\n                        call_pos,\n                    ));\n                }\n                for arg in args {\n                    self.compile_expr(arg)?;\n                }\n                let arg_count = u8::try_from(args.len()).expect(\"argument count exceeds u8\");\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, arg_count);\n            }\n            ArgExprs::Kwargs(kwargs) => {\n                // Check keyword argument count limit\n                if kwargs.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} keyword arguments in function call\"),\n                        call_pos,\n                    ));\n                }\n                // Keyword-only call: compile kwarg values and emit CallFunctionKw\n                let mut kwname_ids = Vec::with_capacity(kwargs.len());\n                for kwarg in kwargs {\n                    self.compile_expr(&kwarg.value)?;\n                    kwname_ids.push(u16::try_from(kwarg.key.name_id.index()).expect(\"name index exceeds u16\"));\n                }\n                self.code.set_location(call_pos, None);\n                self.code.emit_call_function_kw(0, &kwname_ids);\n            }\n            ArgExprs::ArgsKargs {\n                args,\n                var_args,\n                kwargs,\n                var_kwargs,\n            } => {\n                // Mixed positional and keyword arguments - may include *args or **kwargs unpacking\n                if var_args.is_some() || var_kwargs.is_some() {\n                    // Use CallFunctionEx for unpacking - no limit on this path since\n                    // args are built into a tuple dynamically at runtime\n                    self.compile_call_with_unpacking(\n                        callable,\n                        args.as_ref(),\n                        var_args.as_ref(),\n                        kwargs.as_ref(),\n                        var_kwargs.as_ref(),\n                        call_pos,\n                    )?;\n                } else {\n                    // No unpacking - use CallFunctionKw for efficiency\n                    // Check limits before compiling\n                    let pos_count = args.as_ref().map_or(0, Vec::len);\n                    let kw_count = kwargs.as_ref().map_or(0, Vec::len);\n\n                    if pos_count > MAX_CALL_ARGS {\n                        return Err(CompileError::new(\n                            format!(\"more than {MAX_CALL_ARGS} positional arguments in function call\"),\n                            call_pos,\n                        ));\n                    }\n                    if kw_count > MAX_CALL_ARGS {\n                        return Err(CompileError::new(\n                            format!(\"more than {MAX_CALL_ARGS} keyword arguments in function call\"),\n                            call_pos,\n                        ));\n                    }\n\n                    // Compile positional args\n                    if let Some(args) = args {\n                        for arg in args {\n                            self.compile_expr(arg)?;\n                        }\n                    }\n\n                    // Compile kwarg values and collect names\n                    let mut kwname_ids = Vec::new();\n                    if let Some(kwargs) = kwargs {\n                        for kwarg in kwargs {\n                            self.compile_expr(&kwarg.value)?;\n                            kwname_ids.push(u16::try_from(kwarg.key.name_id.index()).expect(\"name index exceeds u16\"));\n                        }\n                    }\n\n                    self.code.set_location(call_pos, None);\n                    self.code.emit_call_function_kw(\n                        u8::try_from(pos_count).expect(\"positional arg count exceeds u8\"),\n                        &kwname_ids,\n                    );\n                }\n            }\n            ArgExprs::GeneralizedCall { args, kwargs } => {\n                // PEP 448: generalized unpacking — multiple *args or **kwargs.\n                // Callable was already pushed above this match; delegate to the helper.\n                let func_name_id = self.get_callable_name_id(callable);\n                self.compile_generalized_call_body(args, kwargs, func_name_id, call_pos)?;\n            }\n        }\n        Ok(())\n    }\n\n    /// Compiles function call arguments and emits the call instruction.\n    ///\n    /// This is used when the callable is already on the stack (e.g., from compiling an expression).\n    /// It compiles the arguments, then emits `CallFunction` or `CallFunctionKw` as appropriate.\n    fn compile_call_args(&mut self, args: &ArgExprs, call_pos: CodeRange) -> Result<(), CompileError> {\n        match args {\n            ArgExprs::Empty => {\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, 0);\n            }\n            ArgExprs::One(arg) => {\n                self.compile_expr(arg)?;\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, 1);\n            }\n            ArgExprs::Two(arg1, arg2) => {\n                self.compile_expr(arg1)?;\n                self.compile_expr(arg2)?;\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, 2);\n            }\n            ArgExprs::Args(args) => {\n                if args.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} positional arguments in function call\"),\n                        call_pos,\n                    ));\n                }\n                for arg in args {\n                    self.compile_expr(arg)?;\n                }\n                let arg_count = u8::try_from(args.len()).expect(\"argument count exceeds u8\");\n                self.code.set_location(call_pos, None);\n                self.code.emit_u8(Opcode::CallFunction, arg_count);\n            }\n            ArgExprs::Kwargs(kwargs) => {\n                if kwargs.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} keyword arguments in function call\"),\n                        call_pos,\n                    ));\n                }\n                let mut kwname_ids = Vec::with_capacity(kwargs.len());\n                for kwarg in kwargs {\n                    self.compile_expr(&kwarg.value)?;\n                    kwname_ids.push(u16::try_from(kwarg.key.name_id.index()).expect(\"name index exceeds u16\"));\n                }\n                self.code.set_location(call_pos, None);\n                self.code.emit_call_function_kw(0, &kwname_ids);\n            }\n            ArgExprs::ArgsKargs {\n                args,\n                kwargs,\n                var_args,\n                var_kwargs,\n            } => {\n                // Mixed positional and keyword arguments - may include *args or **kwargs unpacking\n                if var_args.is_some() || var_kwargs.is_some() {\n                    // Use CallFunctionExtended for unpacking - no limit on this path since\n                    // args are built into a tuple dynamically at runtime.\n                    // Callable is already on stack, so we just need to build args and kwargs.\n                    self.compile_call_args_with_unpacking(\n                        args.as_ref(),\n                        var_args.as_ref(),\n                        kwargs.as_ref(),\n                        var_kwargs.as_ref(),\n                        call_pos,\n                    )?;\n                } else {\n                    // No unpacking - use CallFunctionKw for efficiency\n                    let pos_args = args.as_deref().unwrap_or(&[]);\n                    let kw_args = kwargs.as_deref().unwrap_or(&[]);\n                    let pos_count = pos_args.len();\n                    let kw_count = kw_args.len();\n\n                    // Check limits separately (same as direct calls)\n                    if pos_count > MAX_CALL_ARGS {\n                        return Err(CompileError::new(\n                            format!(\"more than {MAX_CALL_ARGS} positional arguments in function call\"),\n                            call_pos,\n                        ));\n                    }\n                    if kw_count > MAX_CALL_ARGS {\n                        return Err(CompileError::new(\n                            format!(\"more than {MAX_CALL_ARGS} keyword arguments in function call\"),\n                            call_pos,\n                        ));\n                    }\n\n                    // Compile positional args\n                    for arg in pos_args {\n                        self.compile_expr(arg)?;\n                    }\n\n                    // Compile keyword args\n                    let mut kwname_ids = Vec::with_capacity(kw_count);\n                    for kwarg in kw_args {\n                        self.compile_expr(&kwarg.value)?;\n                        kwname_ids.push(u16::try_from(kwarg.key.name_id.index()).expect(\"name index exceeds u16\"));\n                    }\n\n                    self.code.set_location(call_pos, None);\n                    self.code.emit_call_function_kw(\n                        u8::try_from(pos_count).expect(\"positional arg count exceeds u8\"),\n                        &kwname_ids,\n                    );\n                }\n            }\n            ArgExprs::GeneralizedCall { args, kwargs } => {\n                // PEP 448: generalized unpacking — callable is already on the stack.\n                // Use 0xFFFF as func_name_id since we don't know the callee name here.\n                self.compile_generalized_call_body(args, kwargs, 0xFFFF, call_pos)?;\n            }\n        }\n        Ok(())\n    }\n\n    /// Compiles arguments with `*args` and/or `**kwargs` unpacking when callable is already on stack.\n    ///\n    /// This is used for expression calls (e.g., `(lambda *a: a)(*xs)`) where the callable\n    /// is compiled as an expression and is already on the stack.\n    ///\n    /// Stack layout: callable (on stack) -> callable, args_tuple, kwargs_dict?\n    fn compile_call_args_with_unpacking(\n        &mut self,\n        args: Option<&Vec<ExprLoc>>,\n        var_args: Option<&ExprLoc>,\n        kwargs: Option<&Vec<Kwarg>>,\n        var_kwargs: Option<&ExprLoc>,\n        call_pos: CodeRange,\n    ) -> Result<(), CompileError> {\n        // 1. Build args tuple\n        // Push regular positional args and build list\n        let pos_count = args.map_or(0, Vec::len);\n        if let Some(args) = args {\n            for arg in args {\n                self.compile_expr(arg)?;\n            }\n        }\n        self.code.emit_u16(\n            Opcode::BuildList,\n            u16::try_from(pos_count).expect(\"positional arg count exceeds u16\"),\n        );\n\n        // Extend with *args if present\n        if let Some(var_args_expr) = var_args {\n            self.compile_expr(var_args_expr)?;\n            self.code.emit(Opcode::ListExtend);\n        }\n\n        // Convert list to tuple\n        self.code.emit(Opcode::ListToTuple);\n\n        // 2. Build kwargs dict (if we have kwargs or var_kwargs)\n        let has_kwargs = kwargs.is_some() || var_kwargs.is_some();\n        if has_kwargs {\n            // Build dict from regular kwargs\n            let kw_count = kwargs.map_or(0, Vec::len);\n            if let Some(kwargs) = kwargs {\n                for kwarg in kwargs {\n                    // Push key as interned string constant\n                    let key_const = self.code.add_const(Value::InternString(kwarg.key.name_id));\n                    self.code.emit_u16(Opcode::LoadConst, key_const);\n                    // Push value\n                    self.compile_expr(&kwarg.value)?;\n                }\n            }\n            self.code.emit_u16(\n                Opcode::BuildDict,\n                u16::try_from(kw_count).expect(\"keyword count exceeds u16\"),\n            );\n\n            // Merge **kwargs if present\n            // Use 0xFFFF for func_name_id (like builtins) since we don't have a name\n            if let Some(var_kwargs_expr) = var_kwargs {\n                self.compile_expr(var_kwargs_expr)?;\n                self.code.emit_u16(Opcode::DictMerge, 0xFFFF);\n            }\n        }\n\n        // 3. Call the function\n        self.code.set_location(call_pos, None);\n        let flags = u8::from(has_kwargs);\n        self.code.emit_u8(Opcode::CallFunctionExtended, flags);\n        Ok(())\n    }\n\n    /// Compiles arguments for a builtin call and returns the arg count if optimization can be used.\n    ///\n    /// Returns `Some(arg_count)` if the call uses positional-only arguments (CallBuiltinFunction applicable).\n    /// Returns `None` if the call uses kwargs or unpacking (must use standard CallFunction path).\n    ///\n    /// When `Some` is returned, arguments have been compiled onto the stack.\n    fn compile_builtin_call(&mut self, args: &ArgExprs, call_pos: CodeRange) -> Result<Option<u8>, CompileError> {\n        match args {\n            ArgExprs::Empty => Ok(Some(0)),\n            ArgExprs::One(arg) => {\n                self.compile_expr(arg)?;\n                Ok(Some(1))\n            }\n            ArgExprs::Two(arg1, arg2) => {\n                self.compile_expr(arg1)?;\n                self.compile_expr(arg2)?;\n                Ok(Some(2))\n            }\n            ArgExprs::Args(args) => {\n                if args.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} positional arguments in function call\"),\n                        call_pos,\n                    ));\n                }\n                for arg in args {\n                    self.compile_expr(arg)?;\n                }\n                Ok(Some(u8::try_from(args.len()).expect(\"argument count exceeds u8\")))\n            }\n            // Kwargs or unpacking - fall back to standard path\n            ArgExprs::Kwargs(_) | ArgExprs::ArgsKargs { .. } | ArgExprs::GeneralizedCall { .. } => Ok(None),\n        }\n    }\n\n    /// Compiles a function call with `*args` and/or `**kwargs` unpacking.\n    ///\n    /// This generates bytecode to build an args tuple and kwargs dict dynamically,\n    /// then calls the function using `CallFunctionEx`.\n    ///\n    /// Stack layout for call:\n    /// - callable (already on stack)\n    /// - args tuple\n    /// - kwargs dict (if present)\n    fn compile_call_with_unpacking(\n        &mut self,\n        callable: &Callable,\n        args: Option<&Vec<ExprLoc>>,\n        var_args: Option<&ExprLoc>,\n        kwargs: Option<&Vec<Kwarg>>,\n        var_kwargs: Option<&ExprLoc>,\n        call_pos: CodeRange,\n    ) -> Result<(), CompileError> {\n        // Get function name for error messages. Builtins use their real interned name\n        // so duplicate-kwargs errors from **unpacking match CPython.\n        let func_name_id = self.get_callable_name_id(callable);\n\n        // 1. Build args tuple\n        // Push regular positional args and build list\n        let pos_count = args.map_or(0, Vec::len);\n        if let Some(args) = args {\n            for arg in args {\n                self.compile_expr(arg)?;\n            }\n        }\n        self.code.emit_u16(\n            Opcode::BuildList,\n            u16::try_from(pos_count).expect(\"positional arg count exceeds u16\"),\n        );\n\n        // Extend with *args if present\n        if let Some(var_args_expr) = var_args {\n            self.compile_expr(var_args_expr)?;\n            self.code.emit(Opcode::ListExtend);\n        }\n\n        // Convert list to tuple\n        self.code.emit(Opcode::ListToTuple);\n\n        // 2. Build kwargs dict (if we have kwargs or var_kwargs)\n        let has_kwargs = kwargs.is_some() || var_kwargs.is_some();\n        if has_kwargs {\n            // Build dict from regular kwargs\n            let kw_count = kwargs.map_or(0, Vec::len);\n            if let Some(kwargs) = kwargs {\n                for kwarg in kwargs {\n                    // Push key as interned string constant\n                    let key_const = self.code.add_const(Value::InternString(kwarg.key.name_id));\n                    self.code.emit_u16(Opcode::LoadConst, key_const);\n                    // Push value\n                    self.compile_expr(&kwarg.value)?;\n                }\n            }\n            self.code.emit_u16(\n                Opcode::BuildDict,\n                u16::try_from(kw_count).expect(\"keyword count exceeds u16\"),\n            );\n\n            // Merge **kwargs if present\n            if let Some(var_kwargs_expr) = var_kwargs {\n                self.compile_expr(var_kwargs_expr)?;\n                self.code.emit_u16(Opcode::DictMerge, func_name_id);\n            }\n        }\n\n        // 3. Call the function\n        self.code.set_location(call_pos, None);\n        let flags = u8::from(has_kwargs);\n        self.code.emit_u8(Opcode::CallFunctionExtended, flags);\n        Ok(())\n    }\n\n    /// Returns the best available function name id for call-site error messages.\n    ///\n    /// This is primarily used by `DictMerge` during `**kwargs` unpacking so\n    /// duplicate-key and non-mapping errors can mention the actual callee name.\n    /// When the callable is not a named local/global, we still try to resolve\n    /// builtin functions, builtin exception constructors, and builtin types to\n    /// their interned public names.\n    fn get_callable_name_id(&self, callable: &Callable) -> u16 {\n        match callable {\n            Callable::Name(ident) => u16::try_from(ident.name_id.index()).expect(\"name index exceeds u16\"),\n            Callable::Builtin(builtin) => self.get_builtin_name_id(*builtin).unwrap_or(0xFFFF),\n        }\n    }\n\n    /// Resolves a builtin callable to its interned public name, if available.\n    ///\n    /// Returning `None` falls back to `<unknown>` in the VM, which is still\n    /// correct but less helpful. In practice these names should already be\n    /// interned during preparation because builtin names are resolved from source.\n    fn get_builtin_name_id(&self, builtin: Builtins) -> Option<u16> {\n        let name_id = match builtin {\n            Builtins::Function(function) => {\n                let name: &'static str = function.into();\n                self.interns.get_string_id_by_name(name)?\n            }\n            Builtins::ExcType(exc_type) => self.interns.get_string_id_by_name(&exc_type.to_string())?,\n            Builtins::Type(type_) => {\n                let name = type_.builtin_name()?;\n                self.interns.get_string_id_by_name(name)?\n            }\n        };\n\n        u16::try_from(name_id.index()).ok()\n    }\n\n    /// Compiles an attribute call on an object.\n    ///\n    /// The object should already be on the stack. This compiles the arguments\n    /// and emits a CallAttr opcode with the attribute name and arg count.\n    fn compile_method_call(\n        &mut self,\n        attr: &EitherStr,\n        args: &ArgExprs,\n        call_pos: CodeRange,\n    ) -> Result<(), CompileError> {\n        // Get the interned attribute name\n        let name_id = attr.string_id().expect(\"CallAttr requires interned attr name\");\n\n        // Compile arguments based on the argument type\n        match args {\n            ArgExprs::Empty => {\n                self.code.set_location(call_pos, None);\n                self.code.emit_u16_u8(\n                    Opcode::CallAttr,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                    0,\n                );\n            }\n            ArgExprs::One(arg) => {\n                self.compile_expr(arg)?;\n                self.code.set_location(call_pos, None);\n                self.code.emit_u16_u8(\n                    Opcode::CallAttr,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                    1,\n                );\n            }\n            ArgExprs::Two(arg1, arg2) => {\n                self.compile_expr(arg1)?;\n                self.compile_expr(arg2)?;\n                self.code.set_location(call_pos, None);\n                self.code.emit_u16_u8(\n                    Opcode::CallAttr,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                    2,\n                );\n            }\n            ArgExprs::Args(args) => {\n                // Check argument count limit\n                if args.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} arguments in method call\"),\n                        call_pos,\n                    ));\n                }\n                for arg in args {\n                    self.compile_expr(arg)?;\n                }\n                let arg_count = u8::try_from(args.len()).expect(\"argument count exceeds u8\");\n                self.code.set_location(call_pos, None);\n                self.code.emit_u16_u8(\n                    Opcode::CallAttr,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                    arg_count,\n                );\n            }\n            ArgExprs::Kwargs(kwargs) => {\n                // Keyword-only method call\n                if kwargs.len() > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} keyword arguments in method call\"),\n                        call_pos,\n                    ));\n                }\n                // Compile kwarg values and collect names\n                let mut kwname_ids = Vec::with_capacity(kwargs.len());\n                for kwarg in kwargs {\n                    self.compile_expr(&kwarg.value)?;\n                    kwname_ids.push(u16::try_from(kwarg.key.name_id.index()).expect(\"name index exceeds u16\"));\n                }\n                self.code.set_location(call_pos, None);\n                self.code.emit_call_attr_kw(\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                    0, // no positional args\n                    &kwname_ids,\n                );\n            }\n            ArgExprs::ArgsKargs {\n                args,\n                kwargs,\n                var_args,\n                var_kwargs,\n            } => {\n                // Check if there's unpacking - use CallAttrExtended\n                if var_args.is_some() || var_kwargs.is_some() {\n                    return self.compile_method_call_with_unpacking(\n                        name_id,\n                        args.as_ref(),\n                        var_args.as_ref(),\n                        kwargs.as_ref(),\n                        var_kwargs.as_ref(),\n                        call_pos,\n                    );\n                }\n\n                // No unpacking - use CallAttrKw for efficiency\n                let pos_count = args.as_ref().map_or(0, Vec::len);\n                let kw_count = kwargs.as_ref().map_or(0, Vec::len);\n\n                if pos_count > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} positional arguments in method call\"),\n                        call_pos,\n                    ));\n                }\n                if kw_count > MAX_CALL_ARGS {\n                    return Err(CompileError::new(\n                        format!(\"more than {MAX_CALL_ARGS} keyword arguments in method call\"),\n                        call_pos,\n                    ));\n                }\n\n                // Compile positional args\n                if let Some(args) = args {\n                    for arg in args {\n                        self.compile_expr(arg)?;\n                    }\n                }\n\n                // Compile kwarg values and collect names\n                let mut kwname_ids = Vec::new();\n                if let Some(kwargs) = kwargs {\n                    for kwarg in kwargs {\n                        self.compile_expr(&kwarg.value)?;\n                        kwname_ids.push(u16::try_from(kwarg.key.name_id.index()).expect(\"name index exceeds u16\"));\n                    }\n                }\n\n                self.code.set_location(call_pos, None);\n                self.code.emit_call_attr_kw(\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                    u8::try_from(pos_count).expect(\"positional arg count exceeds u8\"),\n                    &kwname_ids,\n                );\n            }\n            ArgExprs::GeneralizedCall { args, kwargs } => {\n                // PEP 448: generalized unpacking on a method call.\n                // Receiver is already on the stack; build args tuple and kwargs dict,\n                // then emit CallAttrExtended.\n                let func_name_id = u16::try_from(name_id.index()).expect(\"name index exceeds u16\");\n                let has_kwargs = !kwargs.is_empty();\n\n                // 1. Build args tuple\n                self.code.emit_u16(Opcode::BuildList, 0);\n                for arg in args {\n                    match arg {\n                        CallArg::Value(e) => {\n                            self.compile_expr(e)?;\n                            self.code.emit_u8(Opcode::ListAppend, 0);\n                        }\n                        CallArg::Unpack(e) => {\n                            self.compile_expr(e)?;\n                            self.code.emit(Opcode::ListExtend);\n                        }\n                    }\n                }\n                self.code.emit(Opcode::ListToTuple);\n\n                // 2. Build kwargs dict (if any)\n                if has_kwargs {\n                    self.code.emit_u16(Opcode::BuildDict, 0);\n                    for kwarg in kwargs {\n                        match kwarg {\n                            CallKwarg::Named(kw) => {\n                                let key_const = self.code.add_const(Value::InternString(kw.key.name_id));\n                                self.code.emit_u16(Opcode::LoadConst, key_const);\n                                self.compile_expr(&kw.value)?;\n                                self.code.emit_u16(Opcode::BuildDict, 1);\n                                self.code.emit_u16(Opcode::DictMerge, func_name_id);\n                            }\n                            CallKwarg::Unpack(e) => {\n                                self.compile_expr(e)?;\n                                self.code.emit_u16(Opcode::DictMerge, func_name_id);\n                            }\n                        }\n                    }\n                }\n\n                // 3. Emit CallAttrExtended\n                self.code.set_location(call_pos, None);\n                let flags = u8::from(has_kwargs);\n                self.code.emit_u16_u8(Opcode::CallAttrExtended, func_name_id, flags);\n            }\n        }\n        Ok(())\n    }\n\n    /// Compiles a method call with `*args` and/or `**kwargs` unpacking.\n    ///\n    /// The receiver object should already be on the stack. This builds the args tuple\n    /// and optional kwargs dict, then emits `CallAttrExtended`.\n    fn compile_method_call_with_unpacking(\n        &mut self,\n        name_id: StringId,\n        args: Option<&Vec<ExprLoc>>,\n        var_args: Option<&ExprLoc>,\n        kwargs: Option<&Vec<Kwarg>>,\n        var_kwargs: Option<&ExprLoc>,\n        call_pos: CodeRange,\n    ) -> Result<(), CompileError> {\n        // 1. Build args tuple\n        // Push regular positional args and build list\n        let pos_count = args.map_or(0, Vec::len);\n        if let Some(args) = args {\n            for arg in args {\n                self.compile_expr(arg)?;\n            }\n        }\n        self.code.emit_u16(\n            Opcode::BuildList,\n            u16::try_from(pos_count).expect(\"positional arg count exceeds u16\"),\n        );\n\n        // Extend with *args if present\n        if let Some(var_args_expr) = var_args {\n            self.compile_expr(var_args_expr)?;\n            self.code.emit(Opcode::ListExtend);\n        }\n\n        // Convert list to tuple\n        self.code.emit(Opcode::ListToTuple);\n\n        // 2. Build kwargs dict (if we have kwargs or var_kwargs)\n        let has_kwargs = kwargs.is_some() || var_kwargs.is_some();\n        if has_kwargs {\n            // Build dict from regular kwargs\n            let kw_count = kwargs.map_or(0, Vec::len);\n            if let Some(kwargs) = kwargs {\n                for kwarg in kwargs {\n                    // Push key as interned string constant\n                    let key_const = self.code.add_const(Value::InternString(kwarg.key.name_id));\n                    self.code.emit_u16(Opcode::LoadConst, key_const);\n                    // Push value\n                    self.compile_expr(&kwarg.value)?;\n                }\n            }\n            self.code.emit_u16(\n                Opcode::BuildDict,\n                u16::try_from(kw_count).expect(\"keyword count exceeds u16\"),\n            );\n\n            // Merge **kwargs if present\n            if let Some(var_kwargs_expr) = var_kwargs {\n                self.compile_expr(var_kwargs_expr)?;\n                // Use the method name for error messages\n                self.code.emit_u16(\n                    Opcode::DictMerge,\n                    u16::try_from(name_id.index()).expect(\"name index exceeds u16\"),\n                );\n            }\n        }\n\n        // 3. Call the method with CallAttrExtended\n        self.code.set_location(call_pos, None);\n        let name_idx = u16::try_from(name_id.index()).expect(\"name index exceeds u16\");\n        let flags = u8::from(has_kwargs);\n        self.code.emit_u16_u8(Opcode::CallAttrExtended, name_idx, flags);\n        Ok(())\n    }\n\n    /// Shared body for PEP 448 generalized calls with multiple `*args` and/or `**kwargs`.\n    ///\n    /// Assumes the callable is already on the stack (pushed by the caller).\n    /// Emits:\n    ///   1. `BuildList(0)` + per-item `ListAppend`/`ListExtend` + `ListToTuple` for args.\n    ///   2. `BuildDict(0)` + per-item `BuildDict(1)+DictMerge`/`DictMerge` for kwargs (if any).\n    ///   3. `CallFunctionExtended(flags)`.\n    ///\n    /// `func_name_id` is used in `DictMerge` error messages; pass `0xFFFF` when unknown.\n    ///\n    /// Stack transition (callable already on stack):\n    ///   `[callable]` → `[callable, args_tuple]` → `[callable, args_tuple, kwargs_dict?]`\n    ///   → `[result]`\n    fn compile_generalized_call_body(\n        &mut self,\n        args: &[CallArg],\n        kwargs: &[CallKwarg],\n        func_name_id: u16,\n        call_pos: CodeRange,\n    ) -> Result<(), CompileError> {\n        // 1. Build args tuple\n        self.code.emit_u16(Opcode::BuildList, 0);\n        for arg in args {\n            match arg {\n                CallArg::Value(e) => {\n                    self.compile_expr(e)?;\n                    self.code.emit_u8(Opcode::ListAppend, 0);\n                }\n                CallArg::Unpack(e) => {\n                    self.compile_expr(e)?;\n                    self.code.emit(Opcode::ListExtend);\n                }\n            }\n        }\n        self.code.emit(Opcode::ListToTuple);\n\n        // 2. Build kwargs dict (if any)\n        let has_kwargs = !kwargs.is_empty();\n        if has_kwargs {\n            // Start with an empty dict, then merge each kwarg one at a time via DictMerge\n            // so that duplicates (including Named+Unpack ordering) raise TypeError correctly.\n            self.code.emit_u16(Opcode::BuildDict, 0);\n            for kwarg in kwargs {\n                match kwarg {\n                    CallKwarg::Named(kw) => {\n                        // Wrap key+value in a single-item dict, then merge into kwargs dict.\n                        let key_const = self.code.add_const(Value::InternString(kw.key.name_id));\n                        self.code.emit_u16(Opcode::LoadConst, key_const);\n                        self.compile_expr(&kw.value)?;\n                        self.code.emit_u16(Opcode::BuildDict, 1);\n                        self.code.emit_u16(Opcode::DictMerge, func_name_id);\n                    }\n                    CallKwarg::Unpack(e) => {\n                        self.compile_expr(e)?;\n                        self.code.emit_u16(Opcode::DictMerge, func_name_id);\n                    }\n                }\n            }\n        }\n\n        // 3. Emit the extended call\n        self.code.set_location(call_pos, None);\n        let flags = u8::from(has_kwargs);\n        self.code.emit_u8(Opcode::CallFunctionExtended, flags);\n        Ok(())\n    }\n\n    /// Compiles a for loop.\n    fn compile_for(\n        &mut self,\n        target: &UnpackTarget,\n        iter: &ExprLoc,\n        body: &[PreparedNode],\n        or_else: &[PreparedNode],\n    ) -> Result<(), CompileError> {\n        // Record stack depth at loop start (before iterator is pushed)\n        // This is the depth we return to when the loop finishes (iterator popped)\n        let loop_exit_depth = self.code.stack_depth();\n\n        // Compile iterator expression\n        self.compile_expr(iter)?;\n        // Convert to iterator\n        self.code.emit(Opcode::GetIter);\n\n        // Loop start\n        let loop_start = self.code.current_offset();\n\n        // Push loop info for break/continue\n        self.loop_stack.push(LoopInfo {\n            start: loop_start,\n            break_jumps: Vec::new(),\n            has_iterator_on_stack: true,\n        });\n\n        // ForIter: advance iterator or jump to end\n        let end_jump = self.code.emit_jump(Opcode::ForIter);\n\n        // Store current value to target (handles both single identifiers and tuple unpacking)\n        self.compile_unpack_target(target);\n\n        // Compile body\n        self.compile_block(body)?;\n\n        // Jump back to loop start\n        self.code.emit_jump_to(Opcode::Jump, loop_start);\n\n        // End of loop - ForIter jumps here when iterator is exhausted\n        self.code.patch_jump(end_jump);\n        // Iterator is popped when loop ends normally, so restore depth to before loop\n        self.code.set_stack_depth(loop_exit_depth);\n\n        // Pop loop info before compiling else block\n        let loop_info = self.loop_stack.pop().expect(\"loop stack underflow\");\n\n        // Compile else block (runs if loop completed without break)\n        if !or_else.is_empty() {\n            self.compile_block(or_else)?;\n        }\n\n        // Patch break jumps to here - AFTER the else block so break skips else\n        for break_jump in loop_info.break_jumps {\n            self.code.patch_jump(break_jump);\n        }\n\n        Ok(())\n    }\n\n    /// Compiles a while loop.\n    ///\n    /// The bytecode structure:\n    /// ```text\n    /// loop_start:\n    ///   [evaluate condition]\n    ///   JumpIfFalse -> end_jump\n    ///   [body]\n    ///   Jump -> loop_start\n    /// end_jump:\n    ///   [else block]\n    /// [break patches here]\n    /// ```\n    ///\n    /// Key differences from `for` loops:\n    /// - No `GetIter` (no iterator)\n    /// - No `ForIter` (use `JumpIfFalse` instead)\n    /// - `continue` jumps to condition evaluation\n    /// - `break` doesn't need to pop iterator (nothing extra on stack)\n    fn compile_while(\n        &mut self,\n        test: &ExprLoc,\n        body: &[PreparedNode],\n        or_else: &[PreparedNode],\n    ) -> Result<(), CompileError> {\n        let loop_start = self.code.current_offset();\n\n        self.loop_stack.push(LoopInfo {\n            start: loop_start,\n            break_jumps: Vec::new(),\n            has_iterator_on_stack: false,\n        });\n\n        self.compile_expr(test)?;\n        let end_jump = self.code.emit_jump(Opcode::JumpIfFalse);\n\n        self.compile_block(body)?;\n        self.code.emit_jump_to(Opcode::Jump, loop_start);\n\n        self.code.patch_jump(end_jump);\n        let loop_info = self.loop_stack.pop().expect(\"loop stack underflow\");\n\n        if !or_else.is_empty() {\n            self.compile_block(or_else)?;\n        }\n\n        for break_jump in loop_info.break_jumps {\n            self.code.patch_jump(break_jump);\n        }\n\n        Ok(())\n    }\n\n    /// Compiles a break statement.\n    ///\n    /// Break exits the innermost loop and skips its else block. If inside a\n    /// try-finally, the finally block must run first.\n    ///\n    /// The bytecode without finally:\n    /// 1. Clean up exception state if inside except handler\n    /// 2. Pop the iterator if in a `for` loop (still on stack during loop body)\n    /// 3. Jump to after the else block\n    ///\n    /// With finally:\n    /// 1. Clean up exception state if inside except handler\n    /// 2. Pop the iterator if in a `for` loop\n    /// 3. Jump to \"finally with break\" path (patched when try compilation completes)\n    /// 4. That path runs finally, then jumps to after the else block\n    fn compile_break(&mut self, position: CodeRange) -> Result<(), CompileError> {\n        if self.loop_stack.is_empty() {\n            return Err(CompileError::new(\"'break' outside loop\", position));\n        }\n\n        // `break` never falls through, but we still compile following statements in the same\n        // block. Preserve the statement-entry depth for that unreachable compilation so\n        // stack-effect tracking remains stable across dead code.\n        let dead_code_depth = self.code.stack_depth();\n        let target_loop_depth = self.loop_stack.len() - 1;\n\n        // If inside except handlers, clean up ALL exception states\n        // Each nested except handler has pushed an exception onto the stack,\n        // so we need to clear/pop each one when breaking out\n        for _ in 0..self.except_handler_depth {\n            self.code.emit(Opcode::ClearException);\n            self.code.emit(Opcode::Pop); // Pop the exception value\n        }\n\n        // Pop the iterator only for `for` loops (has iterator on stack)\n        // `while` loops don't have an iterator to pop\n        if self.loop_stack[target_loop_depth].has_iterator_on_stack {\n            self.code.emit(Opcode::Pop);\n        }\n\n        // Check if we need to go through any finally blocks\n        // We need to run finally if break crosses the try boundary, i.e., if\n        // we're breaking from a loop that existed before the try started.\n        if let Some(finally_target) = self.finally_targets.last_mut()\n            && target_loop_depth < finally_target.loop_depth_at_entry\n        {\n            // Breaking from a loop that's outside (or at the start of) this try-finally,\n            // so finally must run before the break\n            let jump = self.code.emit_jump(Opcode::Jump);\n            finally_target.break_jumps.push(BreakContinueThruFinally {\n                jump,\n                target_loop_depth,\n            });\n            self.code.set_stack_depth(dead_code_depth);\n            return Ok(());\n        }\n\n        // No finally to go through, jump directly to loop end\n        let jump = self.code.emit_jump(Opcode::Jump);\n        self.loop_stack[target_loop_depth].break_jumps.push(jump);\n\n        self.code.set_stack_depth(dead_code_depth);\n\n        Ok(())\n    }\n\n    /// Compiles a continue statement.\n    ///\n    /// Continue jumps back to the loop start (the ForIter instruction) which\n    /// advances the iterator and either enters the next iteration or exits the loop.\n    /// If inside a try-finally, the finally block must run first.\n    fn compile_continue(&mut self, position: CodeRange) -> Result<(), CompileError> {\n        if self.loop_stack.is_empty() {\n            return Err(CompileError::new(\"'continue' not properly in loop\", position));\n        }\n\n        // `continue` never falls through. Preserve the statement-entry stack depth so\n        // subsequent dead statements in this block are compiled with the right abstract stack.\n        let dead_code_depth = self.code.stack_depth();\n        let target_loop_depth = self.loop_stack.len() - 1;\n\n        // If inside except handlers, clean up ALL exception states\n        // Each nested except handler has pushed an exception onto the stack,\n        // so we need to clear/pop each one when continuing\n        for _ in 0..self.except_handler_depth {\n            self.code.emit(Opcode::ClearException);\n            self.code.emit(Opcode::Pop); // Pop the exception value\n        }\n\n        // Check if we need to go through any finally blocks\n        // We need to run finally if continue crosses the try boundary\n        if let Some(finally_target) = self.finally_targets.last_mut()\n            && target_loop_depth < finally_target.loop_depth_at_entry\n        {\n            // Continuing a loop that's outside (or at the start of) this try-finally,\n            // so finally must run before the continue\n            let jump = self.code.emit_jump(Opcode::Jump);\n            finally_target.continue_jumps.push(BreakContinueThruFinally {\n                jump,\n                target_loop_depth,\n            });\n            self.code.set_stack_depth(dead_code_depth);\n            return Ok(());\n        }\n\n        // No finally to go through, jump directly to loop start\n        let loop_start = self.loop_stack[target_loop_depth].start;\n        self.code.emit_jump_to(Opcode::Jump, loop_start);\n\n        self.code.set_stack_depth(dead_code_depth);\n\n        Ok(())\n    }\n\n    /// Compiles break or continue after a finally block has run.\n    ///\n    /// Called from `compile_try` after the finally block code. Each control flow\n    /// statement may target a different loop, so we check if there's another finally\n    /// to go through or if we can jump directly to the loop's target.\n    ///\n    /// Note: All items in the list jumped to the same finally block, so they all\n    /// have the same starting point. After finally runs, we need to route each\n    /// to its target loop, potentially through more finally blocks.\n    fn compile_control_flow_after_finally(&mut self, items: &[BreakContinueThruFinally], is_break: bool) {\n        // All items went through the same finally, now we need to dispatch to\n        // potentially different loops. For simplicity, we assume all items in\n        // a single finally target the same loop (the innermost one at the time).\n        // This is always true since break/continue only targets the innermost loop.\n        let Some(first) = items.first() else {\n            return;\n        };\n        let target_loop_depth = first.target_loop_depth;\n\n        // Check if there's another finally between us and the target loop\n        if let Some(finally_target) = self.finally_targets.last_mut()\n            && target_loop_depth < finally_target.loop_depth_at_entry\n        {\n            // Need to go through another finally\n            let jump = self.code.emit_jump(Opcode::Jump);\n            let jump_info = BreakContinueThruFinally {\n                jump,\n                target_loop_depth,\n            };\n            if is_break {\n                finally_target.break_jumps.push(jump_info);\n            } else {\n                // else continue\n                finally_target.continue_jumps.push(jump_info);\n            }\n            return;\n        }\n\n        // No more finally blocks, jump directly to the loop target\n        if is_break {\n            let jump = self.code.emit_jump(Opcode::Jump);\n            self.loop_stack[target_loop_depth].break_jumps.push(jump);\n        } else {\n            // else continue\n            let loop_start = self.loop_stack[target_loop_depth].start;\n            self.code.emit_jump_to(Opcode::Jump, loop_start);\n        }\n    }\n\n    // ========================================================================\n    // Comprehension Compilation\n    // ========================================================================\n\n    /// Compiles a list comprehension: `[elt for target in iter if cond...]`\n    ///\n    /// Bytecode structure:\n    /// ```text\n    /// BUILD_LIST 0          ; empty result\n    /// <compile first iter>\n    /// GET_ITER\n    /// loop_start:\n    ///   FOR_ITER end_loop\n    ///   STORE_LOCAL target\n    ///   <compile filters - jump back to loop_start if any fails>\n    ///   [nested generators...]\n    ///   <compile elt>\n    ///   LIST_APPEND depth\n    ///   JUMP loop_start\n    /// end_loop:\n    /// ; result list on stack\n    /// ```\n    fn compile_list_comp(&mut self, elt: &ExprLoc, generators: &[Comprehension]) -> Result<(), CompileError> {\n        // Build empty list\n        self.code.emit_u16(Opcode::BuildList, 0);\n\n        // Compile the nested generators, which will eventually append to the list\n        let depth = u8::try_from(generators.len()).expect(\"too many generators in list comprehension\");\n        self.compile_comprehension_generators(generators, 0, |compiler| {\n            compiler.compile_expr(elt)?;\n            compiler.code.emit_u8(Opcode::ListAppend, depth);\n            Ok(())\n        })?;\n\n        Ok(())\n    }\n\n    /// Compiles a set comprehension: `{elt for target in iter if cond...}`\n    fn compile_set_comp(&mut self, elt: &ExprLoc, generators: &[Comprehension]) -> Result<(), CompileError> {\n        // Build empty set\n        self.code.emit_u16(Opcode::BuildSet, 0);\n\n        // Compile the nested generators, which will eventually add to the set\n        let depth = u8::try_from(generators.len()).expect(\"too many generators in set comprehension\");\n        self.compile_comprehension_generators(generators, 0, |compiler| {\n            compiler.compile_expr(elt)?;\n            compiler.code.emit_u8(Opcode::SetAdd, depth);\n            Ok(())\n        })?;\n\n        Ok(())\n    }\n\n    /// Compiles a dict comprehension: `{key: value for target in iter if cond...}`\n    fn compile_dict_comp(\n        &mut self,\n        key: &ExprLoc,\n        value: &ExprLoc,\n        generators: &[Comprehension],\n    ) -> Result<(), CompileError> {\n        // Build empty dict\n        self.code.emit_u16(Opcode::BuildDict, 0);\n\n        // Compile the nested generators, which will eventually set items in the dict\n        let depth = u8::try_from(generators.len()).expect(\"too many generators in dict comprehension\");\n        self.compile_comprehension_generators(generators, 0, |compiler| {\n            compiler.compile_expr(key)?;\n            compiler.compile_expr(value)?;\n            compiler.code.emit_u8(Opcode::DictSetItem, depth);\n            Ok(())\n        })?;\n\n        Ok(())\n    }\n\n    /// Recursively compiles comprehension generators (the for/if clauses).\n    ///\n    /// For each generator:\n    /// 1. Compile the iterator expression and get iterator\n    /// 2. Start loop: FOR_ITER to get next value or exit\n    /// 3. Store to target variable\n    /// 4. Compile filter conditions (jump back to loop start if any fails)\n    /// 5. Either recurse for inner generator, or call the body callback\n    /// 6. Jump back to loop start\n    ///\n    /// The `body_fn` callback is called at the innermost level to emit the element/key-value code.\n    fn compile_comprehension_generators(\n        &mut self,\n        generators: &[Comprehension],\n        index: usize,\n        body_fn: impl FnOnce(&mut Self) -> Result<(), CompileError>,\n    ) -> Result<(), CompileError> {\n        let generator = &generators[index];\n\n        // Record stack depth before iterator expression\n        // This is the depth we return to when the loop finishes (iterator popped)\n        let loop_exit_depth = self.code.stack_depth();\n\n        // Compile iterator expression\n        self.compile_expr(&generator.iter)?;\n        self.code.emit(Opcode::GetIter);\n\n        // Loop start\n        let loop_start = self.code.current_offset();\n\n        // FOR_ITER: advance iterator or jump to end\n        let end_jump = self.code.emit_jump(Opcode::ForIter);\n\n        // Store current value to target (single variable or tuple unpacking)\n        self.compile_unpack_target(&generator.target);\n\n        // Compile filter conditions - jump back to loop start if any fails\n        for cond in &generator.ifs {\n            self.compile_expr(cond)?;\n            // If condition is false, skip to next iteration\n            self.code.emit_jump_to(Opcode::JumpIfFalse, loop_start);\n        }\n\n        // Either recurse for inner generator, or emit body\n        if index + 1 < generators.len() {\n            // Recurse for inner generator\n            self.compile_comprehension_generators(generators, index + 1, body_fn)?;\n        } else {\n            // Innermost level - emit body (the element/key-value expression and append/add/set)\n            body_fn(self)?;\n        }\n\n        // Jump back to loop start\n        self.code.emit_jump_to(Opcode::Jump, loop_start);\n\n        // End of loop\n        self.code.patch_jump(end_jump);\n        // Iterator is popped when loop ends normally, so restore depth to before loop\n        self.code.set_stack_depth(loop_exit_depth);\n\n        Ok(())\n    }\n\n    /// Compiles storage of an unpack target - either a single identifier, nested tuple, or starred.\n    ///\n    /// For single identifiers: emits a simple store.\n    /// For nested tuples: emits `UnpackSequence` (or `UnpackEx` with starred) and recursively\n    /// handles each sub-target.\n    fn compile_unpack_target(&mut self, target: &UnpackTarget) {\n        match target {\n            UnpackTarget::Name(ident) => {\n                // Single identifier - just store directly\n                self.compile_store(ident);\n            }\n            UnpackTarget::Starred(ident) => {\n                // Starred target by itself (shouldn't happen at top level normally)\n                // Just store as if it were a name\n                self.compile_store(ident);\n            }\n            UnpackTarget::Tuple { targets, position } => {\n                // Check if there's a starred target\n                let star_idx = targets.iter().position(|t| matches!(t, UnpackTarget::Starred(_)));\n\n                self.code.set_location(*position, None);\n\n                if let Some(star_idx) = star_idx {\n                    // Has starred target - use UnpackEx\n                    let before = u8::try_from(star_idx).expect(\"too many targets before star\");\n                    let after = u8::try_from(targets.len() - star_idx - 1).expect(\"too many targets after star\");\n                    self.code.emit_u8_u8(Opcode::UnpackEx, before, after);\n                } else {\n                    // No starred target - use UnpackSequence\n                    let count = u8::try_from(targets.len()).expect(\"too many targets in nested unpack\");\n                    self.code.emit_u8(Opcode::UnpackSequence, count);\n                }\n\n                // After UnpackSequence/UnpackEx, values are on stack with first item on top\n                // Store them in order, recursively handling further nesting\n                for target in targets {\n                    self.compile_unpack_target(target);\n                }\n            }\n        }\n    }\n\n    // ========================================================================\n    // Statement Helpers\n    // ========================================================================\n\n    /// Compiles an assert statement.\n    fn compile_assert(&mut self, test: &ExprLoc, msg: Option<&ExprLoc>) -> Result<(), CompileError> {\n        // Compile test\n        self.compile_expr(test)?;\n        // Jump over raise if truthy\n        let skip_jump = self.code.emit_jump(Opcode::JumpIfTrue);\n\n        // Raise AssertionError\n        let exc_idx = self\n            .code\n            .add_const(Value::Builtin(Builtins::ExcType(ExcType::AssertionError)));\n        self.code.emit_u16(Opcode::LoadConst, exc_idx);\n\n        if let Some(msg_expr) = msg {\n            // Call AssertionError(msg)\n            self.compile_expr(msg_expr)?;\n            self.code.emit_u8(Opcode::CallFunction, 1);\n        } else {\n            // Call AssertionError()\n            self.code.emit_u8(Opcode::CallFunction, 0);\n        }\n\n        self.code.emit(Opcode::Raise);\n        self.code.patch_jump(skip_jump);\n        Ok(())\n    }\n\n    /// Compiles f-string parts, returning the number of string parts to concatenate.\n    ///\n    /// Each part is compiled to leave a string value on the stack:\n    /// - `Literal(StringId)`: Push the interned string directly\n    /// - `Interpolation`: Compile expr, emit FormatValue to convert to string\n    fn compile_fstring_parts(&mut self, parts: &[FStringPart]) -> Result<u16, CompileError> {\n        let mut count = 0u16;\n\n        for part in parts {\n            match part {\n                FStringPart::Literal(string_id) => {\n                    // Push the interned string as a constant\n                    let const_idx = self.code.add_const(Value::InternString(*string_id));\n                    self.code.emit_u16(Opcode::LoadConst, const_idx);\n                    count += 1;\n                }\n                FStringPart::Interpolation {\n                    expr,\n                    conversion,\n                    format_spec,\n                    debug_prefix,\n                } => {\n                    // If debug prefix present, push it first\n                    if let Some(prefix_id) = debug_prefix {\n                        let const_idx = self.code.add_const(Value::InternString(*prefix_id));\n                        self.code.emit_u16(Opcode::LoadConst, const_idx);\n                        count += 1;\n                    }\n\n                    // Compile the expression\n                    self.compile_expr(expr)?;\n\n                    // For debug expressions without explicit conversion, Python uses repr by default\n                    let effective_conversion = if debug_prefix.is_some() && matches!(conversion, ConversionFlag::None) {\n                        ConversionFlag::Repr\n                    } else {\n                        *conversion\n                    };\n\n                    // Emit FormatValue with appropriate flags\n                    let flags = self.compile_format_value(effective_conversion, format_spec.as_ref())?;\n                    self.code.emit_u8(Opcode::FormatValue, flags);\n                    count += 1;\n                }\n            }\n        }\n\n        Ok(count)\n    }\n\n    /// Compiles format value flags and optionally pushes format spec to stack.\n    ///\n    /// Returns the flags byte encoding conversion and format spec presence.\n    /// If a format spec is present, it's pushed to the stack before the value.\n    fn compile_format_value(\n        &mut self,\n        conversion: ConversionFlag,\n        format_spec: Option<&FormatSpec>,\n    ) -> Result<u8, CompileError> {\n        // Conversion flag: bits 0-1\n        let conv_bits = match conversion {\n            ConversionFlag::None => 0,\n            ConversionFlag::Str => 1,\n            ConversionFlag::Repr => 2,\n            ConversionFlag::Ascii => 3,\n        };\n\n        match format_spec {\n            None => Ok(conv_bits),\n            Some(FormatSpec::Static(parsed)) => {\n                // Static format spec - push a marker constant with the parsed spec info\n                // We store this as a special format spec value in the constant pool\n                // The VM will recognize this and use the pre-parsed spec\n                let const_idx = self.add_format_spec_const(parsed);\n                self.code.emit_u16(Opcode::LoadConst, const_idx);\n                Ok(conv_bits | 0x04) // has format spec on stack\n            }\n            Some(FormatSpec::Dynamic(dynamic_parts)) => {\n                // Compile dynamic format spec parts to build a format spec string\n                // Then parse it at runtime\n                let part_count = self.compile_fstring_parts(dynamic_parts)?;\n                if part_count > 1 {\n                    self.code.emit_u16(Opcode::BuildFString, part_count);\n                }\n                // Format spec string is now on stack\n                Ok(conv_bits | 0x04) // has format spec on stack\n            }\n        }\n    }\n\n    /// Adds a format spec to the constant pool as an encoded integer.\n    ///\n    /// Uses the encoding from `fstring::encode_format_spec` and stores it as\n    /// a negative integer to distinguish from regular ints.\n    fn add_format_spec_const(&mut self, spec: &ParsedFormatSpec) -> u16 {\n        let encoded = encode_format_spec(spec);\n        // Use negative to distinguish from regular ints (format spec marker)\n        // We negate and subtract 1 to ensure it's negative and recoverable\n        let encoded_i64 = i64::try_from(encoded).expect(\"format spec encoding exceeds i64::MAX\");\n        let marker = -(encoded_i64 + 1);\n        self.code.add_const(Value::Int(marker))\n    }\n\n    // ========================================================================\n    // Exception Handling Compilation\n    // ========================================================================\n\n    /// Compiles a return statement, handling finally blocks properly.\n    ///\n    /// If we're inside a try-finally block, the return value is kept on the stack\n    /// and we jump to a \"finally with return\" section that runs finally then returns.\n    /// Otherwise, we emit a direct `ReturnValue`.\n    fn compile_return(&mut self) {\n        if let Some(finally_target) = self.finally_targets.last_mut() {\n            // Inside a try-finally: jump to finally, then return\n            // Return value is already on stack\n            let jump = self.code.emit_jump(Opcode::Jump);\n            finally_target.return_jumps.push(jump);\n        } else {\n            // Normal return\n            self.code.emit(Opcode::ReturnValue);\n        }\n    }\n\n    /// Compiles a try/except/else/finally block.\n    ///\n    /// The bytecode structure is:\n    /// ```text\n    /// <try_body>                     # protected range\n    /// JUMP to_else_or_finally        # skip handlers if no exception\n    /// handler_dispatch:              # exception pushed by VM\n    ///   # for each handler:\n    ///   <check exception type>\n    ///   <handler body>\n    ///   CLEAR_EXCEPTION\n    ///   JUMP to_finally\n    /// reraise:\n    ///   RERAISE                      # no handler matched\n    /// else_block:\n    ///   <else_body>\n    /// finally_block:\n    ///   <finally_body>\n    /// end:\n    /// ```\n    ///\n    /// For finally blocks, exceptions that propagate through the handler dispatch\n    /// (including RERAISE when no handler matches) are caught by a second exception\n    /// entry that ensures finally runs before propagation.\n    ///\n    /// Returns inside try/except/else jump to a \"finally with return\" path that\n    /// runs the finally code then returns the value.\n    ///\n    /// **Note:** The finally block code is emitted multiple times (once for each\n    /// control flow path: normal, exception, return, break, continue). This is the\n    /// same approach CPython uses - each path has different stack state at entry\n    /// (e.g., return has a value on stack, break has popped the iterator), so we\n    /// can't easily share a single copy. The duplication is intentional.\n    fn compile_try(&mut self, try_block: &Try<PreparedNode>) -> Result<(), CompileError> {\n        let has_finally = !try_block.finally.is_empty();\n        let has_handlers = !try_block.handlers.is_empty();\n        let has_else = !try_block.or_else.is_empty();\n\n        // Record stack depth at try entry (for unwinding on exception)\n        let stack_depth = self.code.stack_depth();\n\n        // If there's a finally block, track returns/break/continue inside try/handlers/else\n        if has_finally {\n            self.finally_targets.push(FinallyTarget {\n                return_jumps: Vec::new(),\n                break_jumps: Vec::new(),\n                continue_jumps: Vec::new(),\n                loop_depth_at_entry: self.loop_stack.len(),\n            });\n        }\n\n        // === Compile try body ===\n        let try_start = self.code.current_offset();\n        self.compile_block(&try_block.body)?;\n        let try_end = self.code.current_offset();\n\n        // Jump to else/finally if no exception (skip handlers)\n        let after_try_jump = self.code.emit_jump(Opcode::Jump);\n\n        // === Handler dispatch starts here ===\n        let handler_start = self.code.current_offset();\n\n        // VM pushes exception onto stack when entering handler.\n        // Adjust compiler's stack depth tracking to reflect this.\n        self.code.adjust_stack_depth(1);\n\n        // Track jumps that go to finally (for patching later)\n        let mut finally_jumps: Vec<JumpLabel> = Vec::new();\n\n        if has_handlers {\n            // Compile exception handlers\n            // handler_entry_depth = stack_depth + 1 (exception on stack)\n            let handler_entry_depth = stack_depth + 1;\n            self.compile_exception_handlers(&try_block.handlers, &mut finally_jumps, handler_entry_depth)?;\n        } else {\n            // No handlers - just reraise (this only happens with try-finally)\n            self.code.emit(Opcode::Reraise);\n        }\n\n        // After handler dispatch, each handler path either:\n        // 1. Matched and popped the exception (via Pop), then jumped to finally\n        // 2. Didn't match and reraised (for last handler)\n        // The handlers' Pop instructions already account for the exception,\n        // so no additional stack depth adjustment is needed here.\n\n        // Mark end of handler dispatch (for finally exception entry)\n        let handler_dispatch_end = self.code.current_offset();\n\n        // === Finally cleanup handler (for exceptions during handler dispatch) ===\n        // This catches exceptions from RERAISE (and any other exceptions in handlers)\n        // and ensures finally runs before the exception propagates.\n        let finally_cleanup_start = if has_finally {\n            let cleanup_start = self.code.current_offset();\n            // Exception value is on stack (pushed by VM), so stack = stack_depth + 1\n            self.code.set_stack_depth(stack_depth + 1);\n            // We need to pop it, run finally, then reraise\n            // But we can't easily save the exception, so we use a different approach:\n            // The exception is already on the exception_stack from handle_exception,\n            // so we can just pop from operand stack, run finally, then reraise.\n            self.code.emit(Opcode::Pop); // Pop exception from operand stack\n            self.compile_block(&try_block.finally)?;\n            self.code.emit(Opcode::Reraise); // Re-raise from exception_stack\n            Some(cleanup_start)\n        } else {\n            None\n        };\n\n        // === Finally with return/break/continue paths ===\n        // Pop finally target and get all the jumps that need to go through finally\n        let finally_with_return_start = if has_finally {\n            let finally_target = self.finally_targets.pop().expect(\"finally_targets should not be empty\");\n\n            // === Finally with return path ===\n            let return_start = if finally_target.return_jumps.is_empty() {\n                None\n            } else {\n                let start = self.code.current_offset();\n                for jump in finally_target.return_jumps {\n                    self.code.patch_jump(jump);\n                }\n                // Return value is on stack, stack = stack_depth + 1\n                self.code.set_stack_depth(stack_depth + 1);\n                self.compile_block(&try_block.finally)?;\n                self.compile_return();\n                Some(start)\n            };\n\n            // === Finally with break path ===\n            // For each break, run finally then either:\n            // - Jump to outer finally's break path (if there's an outer finally between us and the loop)\n            // - Jump directly to the loop's break target\n            if !finally_target.break_jumps.is_empty() {\n                for break_info in &finally_target.break_jumps {\n                    self.code.patch_jump(break_info.jump);\n                }\n                // Break already popped the iterator, so stack = stack_depth - 1\n                // (the iterator was on stack at try entry, break removed it)\n                self.code.set_stack_depth(stack_depth.saturating_sub(1));\n                self.compile_block(&try_block.finally)?;\n                // After finally, compile the break again (handles nested finally or direct jump)\n                self.compile_control_flow_after_finally(&finally_target.break_jumps, true);\n            }\n\n            // === Finally with continue path ===\n            if !finally_target.continue_jumps.is_empty() {\n                for continue_info in &finally_target.continue_jumps {\n                    self.code.patch_jump(continue_info.jump);\n                }\n                // Continue doesn't pop the iterator, stack = stack_depth\n                self.code.set_stack_depth(stack_depth);\n                self.compile_block(&try_block.finally)?;\n                // After finally, compile the continue again (handles nested finally or direct jump)\n                self.compile_control_flow_after_finally(&finally_target.continue_jumps, false);\n            }\n\n            return_start\n        } else {\n            None\n        };\n\n        // === Else block (runs if no exception) ===\n        self.code.patch_jump(after_try_jump);\n        // Normal path from try body, stack = stack_depth\n        self.code.set_stack_depth(stack_depth);\n        let else_start = self.code.current_offset();\n        if has_else {\n            self.compile_block(&try_block.or_else)?;\n        }\n        let else_end = self.code.current_offset();\n\n        // === Normal finally path (no exception pending, no return) ===\n        // Patch all jumps from handlers to go here\n        for jump in finally_jumps {\n            self.code.patch_jump(jump);\n        }\n\n        if has_finally {\n            // Stack = stack_depth (no exception, no return value)\n            self.code.set_stack_depth(stack_depth);\n            self.compile_block(&try_block.finally)?;\n        }\n\n        // === Add exception table entries ===\n        // Order matters: entries are searched in order, so inner entries must come first.\n\n        // Entry 1: Try body -> handler dispatch\n        if has_handlers || has_finally {\n            self.code.add_exception_entry(ExceptionEntry::new(\n                u32::try_from(try_start).expect(\"bytecode offset exceeds u32\"),\n                u32::try_from(try_end).expect(\"bytecode offset exceeds u32\") + 3, // +3 to include the JUMP instruction\n                u32::try_from(handler_start).expect(\"bytecode offset exceeds u32\"),\n                stack_depth,\n            ));\n        }\n\n        // Entry 2: Handler dispatch -> finally cleanup (only if has_finally)\n        // This ensures finally runs when RERAISE is executed or any exception occurs in handlers\n        if let Some(cleanup_start) = finally_cleanup_start {\n            self.code.add_exception_entry(ExceptionEntry::new(\n                u32::try_from(handler_start).expect(\"bytecode offset exceeds u32\"),\n                u32::try_from(handler_dispatch_end).expect(\"bytecode offset exceeds u32\"),\n                u32::try_from(cleanup_start).expect(\"bytecode offset exceeds u32\"),\n                stack_depth,\n            ));\n        }\n\n        // Entry 3: Finally with return -> finally cleanup\n        // If an exception occurs while running finally (in the return path), catch it\n        if let (Some(return_start), Some(cleanup_start)) = (finally_with_return_start, finally_cleanup_start) {\n            self.code.add_exception_entry(ExceptionEntry::new(\n                u32::try_from(return_start).expect(\"bytecode offset exceeds u32\"),\n                u32::try_from(else_start).expect(\"bytecode offset exceeds u32\"), // End at else_start (before else block)\n                u32::try_from(cleanup_start).expect(\"bytecode offset exceeds u32\"),\n                stack_depth,\n            ));\n        }\n\n        // Entry 4: Else block -> finally cleanup (only if has_finally and has_else)\n        // Exceptions in else block should go through finally\n        if has_else && let Some(cleanup_start) = finally_cleanup_start {\n            self.code.add_exception_entry(ExceptionEntry::new(\n                u32::try_from(else_start).expect(\"bytecode offset exceeds u32\"),\n                u32::try_from(else_end).expect(\"bytecode offset exceeds u32\"),\n                u32::try_from(cleanup_start).expect(\"bytecode offset exceeds u32\"),\n                stack_depth,\n            ));\n        }\n\n        Ok(())\n    }\n\n    /// Compiles the exception handlers for a try block.\n    ///\n    /// Each handler checks if the exception matches its type, and if so,\n    /// executes the handler body. If no handler matches, the exception is re-raised.\n    ///\n    /// `handler_entry_depth` is the stack depth when entering handler dispatch\n    /// (i.e., base stack_depth + 1 for the exception value).\n    fn compile_exception_handlers(\n        &mut self,\n        handlers: &[ExceptHandler<PreparedNode>],\n        finally_jumps: &mut Vec<JumpLabel>,\n        handler_entry_depth: u16,\n    ) -> Result<(), CompileError> {\n        // Track jumps from non-matching handlers to next handler\n        let mut next_handler_jumps: Vec<JumpLabel> = Vec::new();\n\n        for (i, handler) in handlers.iter().enumerate() {\n            let is_last = i == handlers.len() - 1;\n\n            // Patch jumps from previous handler's non-match to here\n            // If jumping from a previous handler's no-match, stack has [exc, exc] (duplicate)\n            // We need to pop the duplicate before starting this handler's check\n            if !next_handler_jumps.is_empty() {\n                for jump in next_handler_jumps.drain(..) {\n                    self.code.patch_jump(jump);\n                }\n                // Reset stack depth for jump target: [exc, exc] = handler_entry_depth + 1\n                self.code.set_stack_depth(handler_entry_depth + 1);\n                // Pop the duplicate from previous handler's check\n                self.code.emit(Opcode::Pop);\n            }\n\n            if let Some(exc_type) = &handler.exc_type {\n                // Typed handler: except ExcType: or except ExcType as e:\n                // Stack: [exception]\n\n                // Duplicate exception for type check\n                self.code.emit(Opcode::Dup);\n                // Stack: [exception, exception]\n\n                // Load the exception type to match against\n                self.compile_expr(exc_type)?;\n                // Stack: [exception, exception, exc_type]\n\n                // Check if exception matches the type\n                // This validates exc_type is a valid exception type and performs the match\n                // CheckExcMatch pops exc_type, peeks exception, pushes bool\n                self.code.emit(Opcode::CheckExcMatch);\n                // Stack: [exception, exception, bool]\n\n                // Jump to next handler if match returned False\n                // JumpIfFalse pops the bool, leaving [exception, exception]\n                let no_match_jump = self.code.emit_jump(Opcode::JumpIfFalse);\n\n                if is_last {\n                    // Last handler - if no match, reraise\n                    // But first we need to handle the exception var cleanup\n                } else {\n                    next_handler_jumps.push(no_match_jump);\n                }\n\n                // After JumpIfFalse (match succeeded), stack is [exception, exception]\n                // Pop the duplicate that was used for the type check\n                self.code.emit(Opcode::Pop);\n                // Stack: [exception]\n\n                // Exception matched! Bind to variable if needed\n                if let Some(name) = &handler.name {\n                    // Stack: [exception]\n                    // Store to variable (don't pop - we still need it for current_exception)\n                    self.code.emit(Opcode::Dup);\n                    self.compile_store(name);\n                }\n\n                // Track that we're inside an except handler (for break/continue cleanup)\n                self.except_handler_depth += 1;\n\n                // Compile handler body\n                self.compile_block(&handler.body)?;\n\n                // Exit except handler context\n                self.except_handler_depth -= 1;\n\n                // Delete exception variable (Python 3 behavior)\n                if let Some(name) = &handler.name {\n                    self.compile_delete(name);\n                }\n\n                // Clear current_exception\n                self.code.emit(Opcode::ClearException);\n\n                // Pop the exception from stack\n                self.code.emit(Opcode::Pop);\n\n                // Jump to finally\n                finally_jumps.push(self.code.emit_jump(Opcode::Jump));\n\n                // If this was last handler and no match, we need to reraise\n                if is_last {\n                    self.code.patch_jump(no_match_jump);\n                    // Coming from JumpIfFalse no-match path, stack has [exception, exception]\n                    // Reset stack depth for jump target\n                    self.code.set_stack_depth(handler_entry_depth + 1);\n                    // We need to pop the duplicate before reraising\n                    self.code.emit(Opcode::Pop);\n                    self.code.emit(Opcode::Reraise);\n                }\n            } else {\n                // Bare except: catches everything\n                // Stack: [exception]\n\n                // Bind to variable if needed\n                if let Some(name) = &handler.name {\n                    self.code.emit(Opcode::Dup);\n                    self.compile_store(name);\n                }\n\n                // Track that we're inside an except handler (for break/continue cleanup)\n                self.except_handler_depth += 1;\n\n                // Compile handler body\n                self.compile_block(&handler.body)?;\n\n                // Exit except handler context\n                self.except_handler_depth -= 1;\n\n                // Delete exception variable\n                if let Some(name) = &handler.name {\n                    self.compile_delete(name);\n                }\n\n                // Clear current_exception\n                self.code.emit(Opcode::ClearException);\n\n                // Pop the exception from stack\n                self.code.emit(Opcode::Pop);\n\n                // Jump to finally\n                finally_jumps.push(self.code.emit_jump(Opcode::Jump));\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Compiles deletion of a variable.\n    ///\n    /// At module level, `Local` and `LocalUnassigned` scopes emit `DeleteGlobal`\n    /// because module-level locals live in the globals array.\n    fn compile_delete(&mut self, target: &Identifier) {\n        let slot = u16::try_from(target.namespace_id().index()).expect(\"local slot exceeds u16\");\n        match target.scope {\n            NameScope::Local | NameScope::LocalUnassigned => {\n                if self.is_module_scope {\n                    self.code.emit_u16(Opcode::DeleteGlobal, slot);\n                } else if let Ok(s) = u8::try_from(slot) {\n                    self.code.emit_u8(Opcode::DeleteLocal, s);\n                } else {\n                    // Wide variant not implemented yet\n                    todo!(\"DeleteLocalW for slot > 255\");\n                }\n            }\n            NameScope::Global => {\n                self.code.emit_u16(Opcode::DeleteGlobal, slot);\n            }\n            NameScope::Cell => {\n                // Delete cell not commonly needed\n                // For now, just store None\n                self.code.emit(Opcode::LoadNone);\n                self.compile_store(target);\n            }\n        }\n    }\n}\n\n/// Error that can occur during bytecode compilation.\n///\n/// These are typically limit violations that can't be represented in the bytecode\n/// format (e.g., too many arguments, too many local variables), or import errors\n/// detected at compile time.\n#[derive(Debug, Clone)]\npub struct CompileError {\n    /// Error message describing the issue.\n    message: Cow<'static, str>,\n    /// Source location where the error occurred.\n    position: CodeRange,\n    /// Exception type to use (defaults to SyntaxError).\n    exc_type: ExcType,\n}\n\nimpl CompileError {\n    /// Creates a new compile error with the given message and position.\n    ///\n    /// Defaults to `SyntaxError` exception type.\n    fn new(message: impl Into<Cow<'static, str>>, position: CodeRange) -> Self {\n        Self {\n            message: message.into(),\n            position,\n            exc_type: ExcType::SyntaxError,\n        }\n    }\n\n    /// Converts this compile error into a Python exception.\n    ///\n    /// Uses the stored exception type (SyntaxError or ModuleNotFoundError).\n    /// - SyntaxError: hides the `, in <module>` part (CPython's format)\n    /// - ModuleNotFoundError: hides caret markers (CPython doesn't show them)\n    pub fn into_python_exc(self, filename: &str, source: &str) -> MontyException {\n        let mut frame = if self.exc_type == ExcType::SyntaxError {\n            // SyntaxError uses different format: no `, in <module>`\n            StackFrame::from_position_syntax_error(self.position, filename, source)\n        } else {\n            StackFrame::from_position(self.position, filename, source)\n        };\n        // CPython doesn't show carets for module not found errors\n        if self.exc_type == ExcType::ModuleNotFoundError {\n            frame.hide_caret = true;\n        }\n        MontyException::new_full(self.exc_type, Some(self.message.into_owned()), vec![frame])\n    }\n}\n\n// ============================================================================\n// Operator Mapping Functions\n// ============================================================================\n\n/// Maps a binary `Operator` to its corresponding `Opcode`.\nfn operator_to_opcode(op: &Operator) -> Opcode {\n    match op {\n        Operator::Add => Opcode::BinaryAdd,\n        Operator::Sub => Opcode::BinarySub,\n        Operator::Mult => Opcode::BinaryMul,\n        Operator::Div => Opcode::BinaryDiv,\n        Operator::FloorDiv => Opcode::BinaryFloorDiv,\n        Operator::Mod => Opcode::BinaryMod,\n        Operator::Pow => Opcode::BinaryPow,\n        Operator::MatMult => Opcode::BinaryMatMul,\n        Operator::LShift => Opcode::BinaryLShift,\n        Operator::RShift => Opcode::BinaryRShift,\n        Operator::BitOr => Opcode::BinaryOr,\n        Operator::BitXor => Opcode::BinaryXor,\n        Operator::BitAnd => Opcode::BinaryAnd,\n        // And/Or are handled separately for short-circuit evaluation\n        Operator::And | Operator::Or => {\n            unreachable!(\"And/Or operators handled in compile_binary_op\")\n        }\n    }\n}\n\n/// Maps an `Operator` to its in-place (augmented assignment) `Opcode`.\n///\n/// Returns `None` for operators that don't have an in-place opcode (currently `MatMult`,\n/// since matrix multiplication is not yet supported). Returns `Some(opcode)` for all\n/// other valid augmented assignment operators.\n///\n/// # Panics\n///\n/// Panics if called with `And` or `Or` operators, which cannot be used in augmented\n/// assignments (this would be a parser bug).\nfn operator_to_inplace_opcode(op: &Operator) -> Option<Opcode> {\n    match op {\n        Operator::Add => Some(Opcode::InplaceAdd),\n        Operator::Sub => Some(Opcode::InplaceSub),\n        Operator::Mult => Some(Opcode::InplaceMul),\n        Operator::Div => Some(Opcode::InplaceDiv),\n        Operator::FloorDiv => Some(Opcode::InplaceFloorDiv),\n        Operator::Mod => Some(Opcode::InplaceMod),\n        Operator::Pow => Some(Opcode::InplacePow),\n        Operator::BitAnd => Some(Opcode::InplaceAnd),\n        Operator::BitOr => Some(Opcode::InplaceOr),\n        Operator::BitXor => Some(Opcode::InplaceXor),\n        Operator::LShift => Some(Opcode::InplaceLShift),\n        Operator::RShift => Some(Opcode::InplaceRShift),\n        Operator::MatMult => None,\n        Operator::And | Operator::Or => {\n            unreachable!(\"And/Or operators cannot be used in augmented assignment\")\n        }\n    }\n}\n\n/// Maps a `CmpOperator` to its corresponding `Opcode`.\nfn cmp_operator_to_opcode(op: &CmpOperator) -> Opcode {\n    match op {\n        CmpOperator::Eq => Opcode::CompareEq,\n        CmpOperator::NotEq => Opcode::CompareNe,\n        CmpOperator::Lt => Opcode::CompareLt,\n        CmpOperator::LtE => Opcode::CompareLe,\n        CmpOperator::Gt => Opcode::CompareGt,\n        CmpOperator::GtE => Opcode::CompareGe,\n        CmpOperator::Is => Opcode::CompareIs,\n        CmpOperator::IsNot => Opcode::CompareIsNot,\n        CmpOperator::In => Opcode::CompareIn,\n        CmpOperator::NotIn => Opcode::CompareNotIn,\n        // ModEq is handled specially at the call site (needs constant operand)\n        CmpOperator::ModEq(_) => unreachable!(\"ModEq handled at call site\"),\n    }\n}\n\n/// Returns `true` if any item in the sequence is a PEP 448 unpack (`*expr`).\n///\n/// Used to choose between the fast single-`Build*(N)` path and the generalized\n/// incremental `Build*(0)` + `ListAppend`/`ListExtend` (or `SetAdd`/`SetExtend`) path.\n/// Only the generalized path is needed when at least one `Unpack` variant is present.\nfn has_unpack_seq(items: &[SequenceItem]) -> bool {\n    items.iter().any(|i| matches!(i, SequenceItem::Unpack(_)))\n}\n\n/// Returns `true` if any item in the dict literal is a PEP 448 `**expr` unpack.\n///\n/// Used to choose between the fast single-`BuildDict(N)` path and the generalized\n/// incremental `BuildDict(0)` + `DictSetItem`/`DictUpdate` path.\nfn has_unpack_dict(items: &[DictItem]) -> bool {\n    items.iter().any(|i| matches!(i, DictItem::Unpack(_)))\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/mod.rs",
    "content": "//! Bytecode VM module for Monty.\n//!\n//! This module contains the bytecode representation, compiler, and virtual machine\n//! for executing Python code. The bytecode VM replaces the tree-walking interpreter\n//! with a stack-based execution model.\n//!\n//! # Module Structure\n//!\n//! - `op` - Opcode enum definitions\n//! - `code` - Code object containing bytecode and metadata\n//! - `builder` - CodeBuilder for emitting bytecode during compilation\n//! - `compiler` - AST to bytecode compiler\n//! - `vm` - Virtual machine for bytecode execution\n\nmod builder;\nmod code;\nmod compiler;\nmod op;\nmod vm;\n\npub use code::Code;\npub use compiler::Compiler;\npub(crate) use vm::CallResult;\npub use vm::{FrameExit, VM, VMSnapshot};\n"
  },
  {
    "path": "crates/monty/src/bytecode/op.rs",
    "content": "//! Opcode definitions for the bytecode VM.\n//!\n//! Bytecode is stored as raw `Vec<u8>` for cache efficiency. The `Opcode` enum is a pure\n//! discriminant with no data - operands are fetched separately from the byte stream.\n//!\n//! # Operand Encoding\n//!\n//! - No suffix, 0 bytes: `BinaryAdd`, `Pop`, `LoadNone`\n//! - No suffix, 1 byte (u8/i8): `LoadLocal`, `StoreLocal`, `LoadSmallInt`\n//! - `W` suffix, 2 bytes (u16/i16): `LoadLocalW`, `Jump`, `LoadConst`\n//! - Compound (multiple operands): `CallFunctionKw` (u8 + u8), `MakeClosure` (u16 + u8)\n\nuse strum::FromRepr;\n\n/// Opcode discriminant - just identifies the instruction type.\n///\n/// Operands (if any) follow in the bytecode stream and are fetched separately.\n/// With `#[repr(u8)]`, each opcode is exactly 1 byte. Uses `strum::FromRepr` for\n/// efficient byte-to-opcode conversion (bounds check + transmute).\n///\n/// Opcode bytes are part of Monty's serialized `Code` format, so existing values\n/// must remain stable across releases. Append new opcodes to the end of the enum\n/// instead of inserting them into the middle.\n#[repr(u8)]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr)]\npub enum Opcode {\n    // === Stack Operations (no operand) ===\n    /// Discard top of stack.\n    Pop,\n    /// Duplicate top of stack.\n    Dup,\n    /// Swap top two: [a, b] -> [b, a].\n    Rot2,\n    /// Rotate top three: [a, b, c] -> [c, a, b].\n    Rot3,\n\n    // === Constants & Literals ===\n    /// Push constant from pool. Operand: u16 const_id.\n    LoadConst,\n    /// Push None.\n    LoadNone,\n    /// Push True.\n    LoadTrue,\n    /// Push False.\n    LoadFalse,\n    /// Push small integer (-128 to 127). Operand: i8.\n    LoadSmallInt,\n\n    // === Variables ===\n    // Specialized no-operand versions for common slots (hot path)\n    /// Push local slot 0 (often 'self').\n    LoadLocal0,\n    /// Push local slot 1.\n    LoadLocal1,\n    /// Push local slot 2.\n    LoadLocal2,\n    /// Push local slot 3.\n    LoadLocal3,\n    // General versions with operand\n    /// Push local variable. Operand: u8 slot.\n    LoadLocal,\n    /// Push local (wide, slot > 255). Operand: u16 slot.\n    LoadLocalW,\n    /// Pop and store to local. Operand: u8 slot.\n    StoreLocal,\n    /// Store local (wide). Operand: u16 slot.\n    StoreLocalW,\n    /// Push from global namespace. Operand: u16 slot.\n    LoadGlobal,\n    /// Store to global. Operand: u16 slot.\n    StoreGlobal,\n    /// Load from closure cell. Operand: u16 slot.\n    LoadCell,\n    /// Store to closure cell. Operand: u16 slot.\n    StoreCell,\n    /// Delete local variable. Operand: u8 slot.\n    DeleteLocal,\n    /// Load local in call context: pushes `ExtFunction(name_id)` for undefined names\n    /// instead of yielding `NameLookup`. Operands: u8 slot, u16 name_id.\n    ///\n    /// Used when compiling function calls like `foo()` where `foo` is `LocalUnassigned`.\n    /// If the variable is defined, behaves identically to `LoadLocal`.\n    /// If undefined, pushes an `ExtFunction` value so execution continues to `CallFunction`,\n    /// which naturally yields `FunctionCall` instead of `NameLookup`.\n    /// The name_id is encoded in the operand to avoid namespace lookup ambiguity.\n    LoadLocalCallable,\n    /// Wide variant of `LoadLocalCallable`. Operands: u16 slot, u16 name_id.\n    LoadLocalCallableW,\n    /// Load global in call context: pushes `ExtFunction(name_id)` for undefined names\n    /// instead of yielding `NameLookup`. Operands: u16 slot, u16 name_id.\n    ///\n    /// Used when compiling function calls like `foo()` where `foo` is a global.\n    /// If the variable is defined, behaves identically to `LoadGlobal`.\n    /// If undefined, pushes an `ExtFunction` value so execution continues to `CallFunction`,\n    /// which naturally yields `FunctionCall` instead of `NameLookup`.\n    /// The name_id is encoded in the operand because global and local slot indices\n    /// belong to different namespaces — using the current frame's local_names would\n    /// return the wrong name when called from inside a function.\n    LoadGlobalCallable,\n\n    // === Binary Operations (no operand) ===\n    /// Add: a + b.\n    BinaryAdd,\n    /// Subtract: a - b.\n    BinarySub,\n    /// Multiply: a * b.\n    BinaryMul,\n    /// Divide: a / b.\n    BinaryDiv,\n    /// Floor divide: a // b.\n    BinaryFloorDiv,\n    /// Modulo: a % b.\n    BinaryMod,\n    /// Power: a ** b.\n    BinaryPow,\n    /// Bitwise AND: a & b.\n    BinaryAnd,\n    /// Bitwise OR: a | b.\n    BinaryOr,\n    /// Bitwise XOR: a ^ b.\n    BinaryXor,\n    /// Left shift: a << b.\n    BinaryLShift,\n    /// Right shift: a >> b.\n    BinaryRShift,\n    /// Matrix multiply: a @ b.\n    BinaryMatMul,\n\n    // === Comparison Operations (no operand) ===\n    /// Equal: a == b.\n    CompareEq,\n    /// Not equal: a != b.\n    CompareNe,\n    /// Less than: a < b.\n    CompareLt,\n    /// Less than or equal: a <= b.\n    CompareLe,\n    /// Greater than: a > b.\n    CompareGt,\n    /// Greater than or equal: a >= b.\n    CompareGe,\n    /// Identity: a is b.\n    CompareIs,\n    /// Not identity: a is not b.\n    CompareIsNot,\n    /// Membership: a in b.\n    CompareIn,\n    /// Not membership: a not in b.\n    CompareNotIn,\n    /// Modulo equality: a % b == k (operand: u16 constant index for k).\n    ///\n    /// This is an optimization for patterns like `x % 3 == 0` which are common\n    /// in Python code. Pops b then a, computes `a % b`, then compares with k.\n    CompareModEq,\n\n    // === Unary Operations (no operand) ===\n    /// Logical not: not a.\n    UnaryNot,\n    /// Negation: -a.\n    UnaryNeg,\n    /// Positive: +a.\n    UnaryPos,\n    /// Bitwise invert: ~a.\n    UnaryInvert,\n\n    // === In-place Operations (no operand) ===\n    /// In-place add: a += b.\n    InplaceAdd,\n    /// In-place subtract: a -= b.\n    InplaceSub,\n    /// In-place multiply: a *= b.\n    InplaceMul,\n    /// In-place divide: a /= b.\n    InplaceDiv,\n    /// In-place floor divide: a //= b.\n    InplaceFloorDiv,\n    /// In-place modulo: a %= b.\n    InplaceMod,\n    /// In-place power: a **= b.\n    InplacePow,\n    /// In-place bitwise AND: a &= b.\n    InplaceAnd,\n    /// In-place bitwise OR: a |= b.\n    InplaceOr,\n    /// In-place bitwise XOR: a ^= b.\n    InplaceXor,\n    /// In-place left shift: a <<= b.\n    InplaceLShift,\n    /// In-place right shift: a >>= b.\n    InplaceRShift,\n\n    // === Collection Building ===\n    /// Pop n items, build list. Operand: u16 count.\n    BuildList,\n    /// Pop n items, build tuple. Operand: u16 count.\n    BuildTuple,\n    /// Pop 2n items (k/v pairs), build dict. Operand: u16 count.\n    BuildDict,\n    /// Pop n items, build set. Operand: u16 count.\n    BuildSet,\n    /// Format a value for f-string interpolation. Operand: u8 flags.\n    ///\n    /// Flags encoding:\n    /// - bits 0-1: conversion (0=none, 1=str, 2=repr, 3=ascii)\n    /// - bit 2: has format spec on stack (pop fmt_spec first, then value)\n    /// - bit 3: has static format spec (operand includes u16 const_id after flags)\n    ///\n    /// Pops the value (and optionally format spec), pushes the formatted string.\n    FormatValue,\n    /// Pop n parts, concatenate for f-string. Operand: u16 count.\n    BuildFString,\n    /// Build a slice object from stack values. No operand.\n    ///\n    /// Pops 3 values from stack: step, stop, start (TOS order).\n    /// Each value can be None (for default) or an integer.\n    /// Creates a `HeapData::Slice` and pushes a `Value::Ref` to it.\n    BuildSlice,\n    /// Pop iterable, pop list, extend list with iterable items.\n    ///\n    /// Used for `*args` unpacking: builds a list of positional args,\n    /// then extends it with unpacked iterables.\n    ListExtend,\n    /// Pop TOS (list), push tuple containing the same elements.\n    ///\n    /// Used after building the args list to create the final args tuple\n    /// for `CallFunctionEx`.\n    ListToTuple,\n    /// Pop mapping, pop dict, update dict with mapping. Operand: u16 func_name_id.\n    ///\n    /// Used for `**kwargs` unpacking. The func_name_id is used for error messages\n    /// when the mapping contains non-string keys.\n    DictMerge,\n\n    // === Comprehension Building ===\n    /// Append TOS to list for comprehension. Operand: u8 depth (number of iterators).\n    ///\n    /// Stack: [..., list, iter1, ..., iterN, value] -> [..., list, iter1, ..., iterN]\n    /// Pops value (TOS), appends to list at stack position (len - 2 - depth).\n    /// Depth equals the number of nested iterators (generators) in the comprehension.\n    ListAppend,\n    /// Add TOS to set for comprehension. Operand: u8 depth (number of iterators).\n    ///\n    /// Stack: [..., set, iter1, ..., iterN, value] -> [..., set, iter1, ..., iterN]\n    /// Pops value (TOS), adds to set at stack position (len - 2 - depth).\n    /// May raise TypeError if value is unhashable.\n    SetAdd,\n    /// Set dict[key] = value for comprehension. Operand: u8 depth (number of iterators).\n    ///\n    /// Stack: [..., dict, iter1, ..., iterN, key, value] -> [..., dict, iter1, ..., iterN]\n    /// Pops value (TOS) and key (TOS-1), sets dict[key] = value.\n    /// Dict is at stack position (len - 3 - depth).\n    /// May raise TypeError if key is unhashable.\n    DictSetItem,\n\n    // === Subscript & Attribute ===\n    /// a[b]: pop index, pop obj, push result.\n    BinarySubscr,\n    /// a[b] = c: pop value, pop index, pop obj.\n    StoreSubscr,\n    // NOTE: DeleteSubscr removed - `del` statement not supported by parser\n    /// Pop obj, push obj.attr. Operand: u16 name_id.\n    LoadAttr,\n    /// Pop module, push module.attr for `from ... import`. Operand: u16 name_id.\n    ///\n    /// Like `LoadAttr` but raises `ImportError` instead of `AttributeError`\n    /// when the attribute is not found. Used for `from module import name`.\n    LoadAttrImport,\n    /// Pop value, pop obj, set obj.attr. Operand: u16 name_id.\n    StoreAttr,\n    // NOTE: DeleteAttr removed - `del` statement not supported by parser\n\n    // === Function Calls ===\n    /// Call TOS with n positional args. Operand: u8 arg_count.\n    CallFunction,\n    /// Call a builtin function directly. Operands: u8 builtin_id, u8 arg_count.\n    ///\n    /// The builtin_id is the discriminant of `BuiltinsFunctions` (via `FromRepr`).\n    /// This is an optimization over `LoadConst + CallFunction` that avoids:\n    /// - Constant pool lookup\n    /// - Pushing/popping the callable on the stack\n    /// - Runtime type dispatch in call_function\n    CallBuiltinFunction,\n    /// Call a builtin type constructor directly. Operands: u8 type_id, u8 arg_count.\n    ///\n    /// The type_id is the discriminant of `BuiltinsTypes` (via `FromRepr`).\n    /// This is an optimization for type constructors like `list()`, `int()`, `str()`.\n    CallBuiltinType,\n    /// Call with positional and keyword args.\n    ///\n    /// Operands: u8 pos_count, u8 kw_count, then kw_count u16 name indices.\n    ///\n    /// Stack: [callable, pos_args..., kw_values...]\n    /// After the two count bytes, there are kw_count little-endian u16 values,\n    /// each being a StringId index for the corresponding keyword argument name.\n    CallFunctionKw,\n    /// Call attribute on object. Operands: u16 name_id, u8 arg_count.\n    ///\n    /// This is used for both method calls (`obj.method(args)`) and module\n    /// attribute calls (`module.func(args)`). The attribute is looked up\n    /// on the object and called with the given arguments.\n    CallAttr,\n    /// Call attribute with keyword args. Operands: u16 name_id, u8 pos_count, u8 kw_count, then kw_count u16 name indices.\n    ///\n    /// Stack: [obj, pos_args..., kw_values...]\n    /// After the operands, there are kw_count little-endian u16 values,\n    /// each being a StringId index for the corresponding keyword argument name.\n    CallAttrKw,\n    /// Call a defined function with *args tuple and **kwargs dict. Operand: u8 flags.\n    ///\n    /// Flags:\n    /// - bit 0: has kwargs dict on stack\n    ///\n    /// Stack layout (bottom to top):\n    /// - callable\n    /// - args tuple\n    /// - kwargs dict (if flag bit 0 set)\n    ///\n    /// Used for calls with `*args` and/or `**kwargs` unpacking.\n    CallFunctionExtended,\n    /// Call attribute with *args tuple and **kwargs dict. Operands: u16 name_id, u8 flags.\n    ///\n    /// Flags:\n    /// - bit 0: has kwargs dict on stack\n    ///\n    /// Stack layout (bottom to top):\n    /// - receiver object\n    /// - args tuple\n    /// - kwargs dict (if flag bit 0 set)\n    ///\n    /// Used for method calls with `*args` and/or `**kwargs` unpacking.\n    CallAttrExtended,\n\n    // === Control Flow ===\n    /// Unconditional relative jump. Operand: i16 offset.\n    Jump,\n    /// Jump if TOS truthy, always pop. Operand: i16 offset.\n    JumpIfTrue,\n    /// Jump if TOS falsy, always pop. Operand: i16 offset.\n    JumpIfFalse,\n    /// Jump if TOS truthy (keep), else pop. Operand: i16 offset.\n    JumpIfTrueOrPop,\n    /// Jump if TOS falsy (keep), else pop. Operand: i16 offset.\n    JumpIfFalseOrPop,\n\n    // === Iteration ===\n    /// Convert TOS to iterator.\n    GetIter,\n    /// Advance iterator or jump to end. Operand: i16 offset.\n    ForIter,\n\n    // === Function Definition ===\n    /// Create function object. Operand: u16 func_id.\n    MakeFunction,\n    /// Create closure. Operands: u16 func_id, u8 cell_count.\n    MakeClosure,\n\n    // === Exception Handling ===\n    // Note: No SetupTry/PopExceptHandler - we use static exception_table\n    /// Raise TOS as exception.\n    Raise,\n    // NOTE: RaiseFrom removed - `raise ... from ...` not supported by parser\n    /// Re-raise current exception (bare `raise`).\n    Reraise,\n    /// Clear current_exception when exiting except block.\n    ClearException,\n    /// Check if exception matches type for except clause.\n    ///\n    /// Stack: [..., exception, exc_type] -> [..., exception, bool]\n    /// Validates that exc_type is a valid exception type (ExcType or tuple of ExcTypes).\n    /// If invalid, raises TypeError. If valid, pushes True if exception matches, else False.\n    CheckExcMatch,\n\n    // === Return ===\n    /// Return TOS from function.\n    ReturnValue,\n\n    // === Async/Await ===\n    /// Await the TOS value.\n    ///\n    /// Handles `ExternalFuture`, `Coroutine`, and `GatherFuture` awaitables.\n    /// For `ExternalFuture`: if resolved, pushes result; if pending, blocks task.\n    /// For `Coroutine`: validates state is `New`, then starts execution.\n    /// For `GatherFuture`: spawns all coroutines as tasks and blocks until completion.\n    ///\n    /// Raises `TypeError` if TOS is not awaitable.\n    /// Raises `RuntimeError` if coroutine/future has already been awaited.\n    Await,\n\n    // === Unpacking ===\n    /// Unpack TOS into n values. Operand: u8 count.\n    UnpackSequence,\n    /// Unpack with *rest. Operands: u8 before, u8 after.\n    UnpackEx,\n\n    // === Special ===\n    /// No operation (for patching/alignment).\n    Nop,\n\n    // === Module Operations ===\n    /// Load a built-in module onto the stack. Operand: u8 module_id.\n    ///\n    /// The module_id maps to `BuiltinModule` (0=sys, 1=typing).\n    /// Creates the module on the heap and pushes a `Value::Ref` to it.\n    LoadModule,\n    /// Raises `ModuleNotFoundError` at runtime. Operand: u16 constant index for module name.\n    ///\n    /// This opcode is emitted when the compiler encounters an import of an unknown module.\n    /// Instead of failing at compile time, the error is deferred to runtime so that\n    /// imports inside `if TYPE_CHECKING:` blocks or other non-executed code paths\n    /// don't cause errors.\n    ///\n    /// The operand is an index into the constant pool where the module name string is stored.\n    RaiseImportError,\n    /// Duplicate the top two stack values, preserving order: `[a, b] -> [a, b, a, b]`.\n    ///\n    /// Appended at the end to preserve the serialized byte values of all older opcodes.\n    Dup2,\n    /// Delete global variable (set to Undefined). Operand: u16 slot.\n    ///\n    /// Appended at the end to preserve the serialized byte values of all older opcodes.\n    DeleteGlobal,\n\n    /// Pop a mapping, silently merge into the dict at `depth`. Operand: u8 depth.\n    ///\n    /// Used for `**expr` unpack inside dict literals, where later keys overwrite earlier ones\n    /// (unlike `DictMerge` which raises `TypeError` on duplicate keys).\n    ///\n    /// Stack: [..., dict, iter1, ..., iterN, mapping] -> [..., dict, iter1, ..., iterN]\n    /// Pops mapping (TOS), merges into dict at stack position `len - 2 - depth`.\n    /// Raises `TypeError` if `mapping` is not a dict.\n    DictUpdate,\n    /// Pop an iterable, add all items to set at `depth`. Operand: u8 depth.\n    ///\n    /// Used for `*expr` unpack inside set literals (e.g., `{*a, 1}`).\n    /// Follows the same depth convention as `ListAppend`/`SetAdd`.\n    ///\n    /// Stack: [..., set, iter1, ..., iterN, iterable] -> [..., set, iter1, ..., iterN]\n    /// Pops iterable (TOS), adds each item to set at stack position `len - 2 - depth`.\n    /// Raises `TypeError` if iterable is not iterable.\n    SetExtend,\n}\n\nimpl TryFrom<u8> for Opcode {\n    type Error = InvalidOpcodeError;\n\n    fn try_from(byte: u8) -> Result<Self, Self::Error> {\n        Self::from_repr(byte).ok_or(InvalidOpcodeError(byte))\n    }\n}\n\nimpl Opcode {\n    /// Returns the stack effect of this opcode (positive = push, negative = pop).\n    ///\n    /// Some opcodes have variable effects (e.g., `BuildList` depends on its operand).\n    /// For those, this returns `None` and the caller must compute the effect.\n    ///\n    /// For opcodes that have known, fixed stack effects, returns `Some(i16)`.\n    #[must_use]\n    pub const fn stack_effect(self) -> Option<i16> {\n        #![expect(clippy::allow_attributes, reason = \"expect seems broken with enum_glob_use\")]\n        #[allow(clippy::enum_glob_use, reason = \"simplifies churn\")]\n        use Opcode::*;\n        Some(match self {\n            // Stack operations\n            Pop => -1,\n            Dup => 1,\n            Dup2 => 2,\n            Rot2 | Rot3 => 0, // reorder, no net change\n\n            // Constants & Literals (all push 1)\n            LoadConst | LoadNone | LoadTrue | LoadFalse | LoadSmallInt => 1,\n\n            // Variables - loads push, stores pop\n            LoadLocal0 | LoadLocal1 | LoadLocal2 | LoadLocal3 => 1,\n            LoadLocal | LoadLocalW | LoadLocalCallable | LoadLocalCallableW | LoadGlobal | LoadGlobalCallable\n            | LoadCell => 1,\n            StoreLocal | StoreLocalW | StoreGlobal | StoreCell => -1,\n            DeleteLocal | DeleteGlobal => 0, // doesn't affect stack\n\n            // Binary operations: pop 2, push 1 = -1\n            BinaryAdd | BinarySub | BinaryMul | BinaryDiv | BinaryFloorDiv | BinaryMod | BinaryPow | BinaryAnd\n            | BinaryOr | BinaryXor | BinaryLShift | BinaryRShift | BinaryMatMul => -1,\n\n            // Comparisons: pop 2, push 1 = -1\n            CompareEq | CompareNe | CompareLt | CompareLe | CompareGt | CompareGe | CompareIs | CompareIsNot\n            | CompareIn | CompareNotIn | CompareModEq => -1,\n\n            // Unary operations: pop 1, push 1 = 0\n            UnaryNot | UnaryNeg | UnaryPos | UnaryInvert => 0,\n\n            // In-place operations: pop 1 (rhs), leave target on stack = -1\n            InplaceAdd | InplaceSub | InplaceMul | InplaceDiv | InplaceFloorDiv | InplaceMod | InplacePow\n            | InplaceAnd | InplaceOr | InplaceXor | InplaceLShift | InplaceRShift => -1,\n\n            // Collection building - depends on operand, return None\n            BuildList | BuildTuple | BuildDict | BuildSet | BuildFString => return None,\n            // FormatValue: pops 1 value (+ optional fmt_spec), pushes 1. Variable.\n            FormatValue => return None,\n            // BuildSlice: pop 3, push 1 = -2\n            BuildSlice => -2,\n            // ListExtend: pop 2 (iterable + list), push 1 (list) = -1\n            ListExtend => -1,\n            // ListToTuple: pop 1, push 1 = 0\n            ListToTuple => 0,\n            // DictMerge: pop 2, push 1 = -1\n            DictMerge => -1,\n\n            // Comprehension building - pops value, no push (stores in collection below)\n            ListAppend | SetAdd => -1,\n            DictSetItem => -2, // pops key and value\n\n            // Subscript & Attribute\n            BinarySubscr => -1,             // pop 2, push 1\n            StoreSubscr => -3,              // pop 3, push 0\n            LoadAttr | LoadAttrImport => 0, // pop 1, push 1\n            StoreAttr => -2,                // pop 2, push 0\n\n            // Function calls - depend on arg count\n            CallFunction | CallBuiltinFunction | CallBuiltinType | CallFunctionKw | CallAttr | CallAttrKw\n            | CallFunctionExtended | CallAttrExtended => return None,\n\n            // Control flow - no stack effect (jumps don't push/pop)\n            Jump => 0,\n            JumpIfTrue | JumpIfFalse => -1,                    // always pop condition\n            JumpIfTrueOrPop | JumpIfFalseOrPop => return None, // variable (0 or -1)\n\n            // Iteration\n            GetIter => 0,           // pop iterable, push iterator\n            ForIter => return None, // pushes value or jumps (variable)\n\n            // Async/await\n            Await => 0, // pop awaitable, push result\n\n            // Function definition - push 1 (the function/closure)\n            MakeFunction | MakeClosure => 1,\n\n            // Exception handling\n            Raise => -1,         // pop exception\n            Reraise => 0,        // no stack change (reads from exception_stack)\n            ClearException => 0, // clears exception_stack, no operand stack change\n            CheckExcMatch => 0,  // pop exc_type, push bool (net 0, but exc stays)\n\n            // Return\n            ReturnValue => -1,\n\n            // Unpacking - depends on operand\n            UnpackSequence | UnpackEx => return None,\n\n            // Dict/set literal extensions (PEP 448):\n            // DictUpdate: pop mapping, silently merge into dict below = -1\n            DictUpdate => -1,\n            // SetExtend: pop iterable, add all items to set below = -1\n            SetExtend => -1,\n\n            // Special\n            Nop => 0,\n\n            // Module\n            LoadModule => 1,       // push module\n            RaiseImportError => 0, // raises exception, no stack change before that\n        })\n    }\n}\n\n/// Error returned when attempting to convert an invalid byte to an Opcode.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct InvalidOpcodeError(pub u8);\n\nimpl std::fmt::Display for InvalidOpcodeError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"invalid opcode byte: {}\", self.0)\n    }\n}\n\nimpl std::error::Error for InvalidOpcodeError {}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_opcode_roundtrip() {\n        // Verify that all opcodes from 0 to DeleteGlobal (last opcode) can be converted to u8 and back.\n        for byte in 0..=Opcode::DeleteGlobal as u8 {\n            let opcode = Opcode::try_from(byte).unwrap();\n            assert_eq!(opcode as u8, byte, \"opcode {opcode:?} has wrong discriminant\");\n        }\n    }\n\n    #[test]\n    fn test_serialized_opcode_values_remain_stable() {\n        // `RaiseImportError` was the tail opcode before `Dup2` was introduced. Keeping it at\n        // byte 110 preserves compatibility for serialized runners and snapshots compiled by\n        // older versions.\n        assert_eq!(Opcode::RaiseImportError as u8, 110);\n        assert_eq!(Opcode::Dup2 as u8, 111);\n        assert_eq!(Opcode::DeleteGlobal as u8, 112);\n        assert_eq!(Opcode::DictUpdate as u8, 113);\n        assert_eq!(Opcode::SetExtend as u8, 114);\n    }\n\n    #[test]\n    fn test_invalid_opcode() {\n        // Byte just after the last valid opcode should fail\n        let result = Opcode::try_from(Opcode::SetExtend as u8 + 1);\n        assert!(result.is_err());\n        // 255 should also fail\n        let result = Opcode::try_from(255u8);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_opcode_size() {\n        // Verify opcode is 1 byte\n        assert_eq!(std::mem::size_of::<Opcode>(), 1);\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/async_exec.rs",
    "content": "//! Async execution support for the VM.\n//!\n//! This module contains all async-related methods for the VM including:\n//! - Awaiting coroutines, external futures, and gather futures\n//! - Task scheduling and context switching\n//! - Task completion and failure handling\n//! - External future resolution\n\nuse super::{AwaitResult, CallFrame, VM};\nuse crate::{\n    InvalidInputError, MontyObject,\n    args::ArgValues,\n    asyncio::{CallId, CoroutineState, GatherItem, TaskId},\n    bytecode::vm::scheduler::{PendingCallData, SerializedTaskFrame, TaskState},\n    defer_drop,\n    exception_private::{ExcType, RunError, SimpleException},\n    heap::{HeapData, HeapGuard, HeapId},\n    heap_data::HeapDataMut,\n    intern::FunctionId,\n    resource::ResourceTracker,\n    types::{List, PyTrait},\n    value::Value,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Executes the Await opcode.\n    ///\n    /// Pops the awaitable from the stack and handles it based on its type:\n    /// - `Coroutine`: validates state is New, then pushes a frame to execute it\n    /// - `ExternalFuture`: blocks until resolved or yields if not ready\n    /// - `GatherFuture`: spawns tasks for coroutines and tracks external futures\n    ///\n    /// Returns `AwaitResult` indicating what action the VM should take.\n    pub(super) fn exec_get_awaitable(&mut self) -> Result<AwaitResult, RunError> {\n        let awaitable = self.pop();\n\n        let mut awaitable_guard = HeapGuard::new(awaitable, self);\n        let (awaitable, this) = awaitable_guard.as_parts();\n\n        match awaitable {\n            Value::Ref(heap_id) => {\n                let heap_id = *heap_id;\n                let heap_data_type = match this.heap.get(heap_id) {\n                    HeapData::Coroutine(_) => Some(AwaitableType::Coroutine),\n                    HeapData::GatherFuture(_) => Some(AwaitableType::GatherFuture),\n                    _ => None,\n                };\n\n                match heap_data_type {\n                    Some(AwaitableType::Coroutine) => {\n                        let (awaitable, this) = awaitable_guard.into_parts();\n                        this.await_coroutine(heap_id, awaitable)\n                    }\n                    Some(AwaitableType::GatherFuture) => {\n                        let (awaitable, this) = awaitable_guard.into_parts();\n                        this.await_gather_future(heap_id, awaitable)\n                    }\n                    None => Err(ExcType::object_not_awaitable(awaitable.py_type(this.heap))),\n                }\n            }\n            &Value::ExternalFuture(call_id) => this.await_external_future(call_id),\n            _ => Err(ExcType::object_not_awaitable(awaitable.py_type(this.heap))),\n        }\n    }\n\n    /// Awaits a coroutine by pushing a frame to execute it.\n    ///\n    /// Validates the coroutine is in `New` state, extracts its captured namespace\n    /// and cells, marks it as `Running`, and pushes a frame to execute the coroutine body.\n    fn await_coroutine(&mut self, heap_id: HeapId, awaitable: Value) -> Result<AwaitResult, RunError> {\n        let this = self;\n        defer_drop!(awaitable, this);\n\n        let HeapData::Coroutine(coro) = this.heap.get(heap_id) else {\n            unreachable!(\"await_coroutine called with non-coroutine heap_id\")\n        };\n\n        // Check if coroutine can be awaited (must be New)\n        if coro.state != CoroutineState::New {\n            return Err(\n                SimpleException::new_msg(ExcType::RuntimeError, \"cannot reuse already awaited coroutine\").into(),\n            );\n        }\n\n        // Extract coroutine data before mutating\n        let func_id = coro.func_id;\n        let namespace_values: Vec<Value> = coro.namespace.iter().map(|v| v.clone_with_heap(this.heap)).collect();\n\n        // Mark coroutine as Running\n        if let HeapDataMut::Coroutine(coro_mut) = this.heap.get_mut(heap_id) {\n            coro_mut.state = CoroutineState::Running;\n        }\n\n        // Create namespace and push frame (guard drops awaitable at scope exit)\n        this.start_coroutine_frame(func_id, namespace_values)?;\n\n        Ok(AwaitResult::FramePushed)\n    }\n\n    /// Awaits a gather future by spawning tasks for coroutines and tracking external futures.\n    ///\n    /// For each item in the gather:\n    /// - Coroutines are spawned as tasks\n    /// - External futures are checked for resolution or registered for tracking\n    ///\n    /// If all items are already resolved, returns immediately. Otherwise blocks\n    /// the current task and switches to a ready task or yields to the host.\n    fn await_gather_future(&mut self, heap_id: HeapId, awaitable: Value) -> Result<AwaitResult, RunError> {\n        let this = self;\n        let mut awaitable_guard = HeapGuard::new(awaitable, this);\n        let (_, this) = awaitable_guard.as_parts();\n\n        let HeapData::GatherFuture(gather) = this.heap.get(heap_id) else {\n            unreachable!(\"await_gather_future called with non-gather heap_id\")\n        };\n\n        // Check if already being waited on (double-await)\n        if gather.waiter.is_some() {\n            return Err(SimpleException::new_msg(ExcType::RuntimeError, \"cannot reuse already awaited gather\").into());\n        }\n\n        // If no items to gather, return empty list immediately\n        if gather.item_count() == 0 {\n            let list_id = this.heap.allocate(HeapData::List(List::new(vec![])))?;\n            return Ok(AwaitResult::ValueReady(Value::Ref(list_id)));\n        }\n\n        // Set waiter and clone items to process\n        // Note: We clone instead of mem::take because GatherItem::Coroutine holds HeapIds\n        // that need to stay in gather.items for proper ref counting when the gather is dropped.\n        let current_task = this.scheduler.current_task_id();\n        let items: Vec<GatherItem> = if let HeapDataMut::GatherFuture(gather_mut) = this.heap.get_mut(heap_id) {\n            gather_mut.waiter = current_task;\n            gather_mut.items.clone()\n        } else {\n            vec![]\n        };\n\n        // Process each item\n        let mut task_ids = Vec::new();\n        let mut pending_calls = Vec::new();\n\n        for (idx, item) in items.iter().enumerate() {\n            match item {\n                GatherItem::Coroutine(coro_id) => {\n                    // Spawn as task with the item index as result index\n                    let task_id = this.scheduler.spawn(*coro_id, Some(heap_id), Some(idx));\n                    task_ids.push(task_id);\n                }\n                GatherItem::ExternalFuture(call_id) => {\n                    // Check if already resolved\n                    this.scheduler.mark_consumed(*call_id);\n\n                    if let Some(value) = this.scheduler.take_resolved(*call_id) {\n                        // Already resolved - store result immediately\n                        if let HeapDataMut::GatherFuture(gather_mut) = this.heap.get_mut(heap_id) {\n                            gather_mut.results[idx] = Some(value);\n                        }\n                    } else {\n                        // Not resolved yet - track it\n                        pending_calls.push(*call_id);\n                        // Register gather as waiting on this call\n                        this.scheduler.register_gather_for_call(*call_id, heap_id, idx);\n                    }\n                }\n            }\n        }\n\n        // Store task IDs and pending calls in the gather\n        if let HeapDataMut::GatherFuture(gather_mut) = this.heap.get_mut(heap_id) {\n            gather_mut.task_ids = task_ids;\n            gather_mut.pending_calls.clone_from(&pending_calls);\n        }\n\n        // Check if all items are already complete (only external futures, all resolved)\n        let all_complete = {\n            if let HeapData::GatherFuture(gather) = this.heap.get(heap_id) {\n                gather.task_ids.is_empty() && gather.pending_calls.is_empty()\n            } else {\n                false\n            }\n        };\n\n        if all_complete {\n            // All external futures were already resolved - return results immediately\n            // Steal results using mem::take - avoids refcount dance since we're dropping\n            // the GatherFuture anyway via awaitable.drop_with_heap below\n            let results: Vec<Value> = if let HeapDataMut::GatherFuture(gather) = this.heap.get_mut(heap_id) {\n                std::mem::take(&mut gather.results)\n                    .into_iter()\n                    .map(|r| r.expect(\"all results should be filled\"))\n                    .collect()\n            } else {\n                vec![]\n            };\n\n            let list_id = this.heap.allocate(HeapData::List(List::new(results)))?;\n            return Ok(AwaitResult::ValueReady(Value::Ref(list_id)));\n        }\n\n        // Block current task on this gather\n        this.scheduler.block_current_on_gather(heap_id);\n\n        // Consume the awaitable without decrementing refcount - the GatherFuture\n        // must stay alive for result collection. It will be dec_ref'd when\n        // the gather completes (in handle_task_completion).\n        let (awaitable, this) = awaitable_guard.into_parts();\n        #[cfg_attr(\n            not(feature = \"ref-count-panic\"),\n            expect(clippy::forget_non_drop, reason = \"has Drop with ref-count-panic feature\")\n        )]\n        std::mem::forget(awaitable);\n\n        // Switch to next ready task (spawned tasks) or yield for external futures\n        this.switch_or_yield()\n    }\n\n    /// Awaits an external future by blocking until it's resolved.\n    ///\n    /// If the future is already resolved, returns the value immediately.\n    /// Otherwise blocks the current task and switches to a ready task or yields to the host.\n    fn await_external_future(&mut self, call_id: CallId) -> Result<AwaitResult, RunError> {\n        // Check if already consumed (double-await error)\n        if self.scheduler.is_consumed(call_id) {\n            return Err(SimpleException::new_msg(ExcType::RuntimeError, \"cannot reuse already awaited future\").into());\n        }\n\n        // Mark as consumed\n        self.scheduler.mark_consumed(call_id);\n\n        // Check if the future is already resolved\n        if let Some(value) = self.scheduler.take_resolved(call_id) {\n            Ok(AwaitResult::ValueReady(value))\n        } else {\n            // Block current task on this call\n            self.scheduler.block_current_on_call(call_id);\n\n            // Switch to next ready task or yield to host\n            self.switch_or_yield()\n        }\n    }\n\n    /// Starts execution of a coroutine by pushing its locals onto the stack.\n    ///\n    /// Extends the VM stack with the coroutine's pre-bound namespace values\n    /// and pushes a new frame to execute the coroutine's function body.\n    fn start_coroutine_frame(&mut self, func_id: FunctionId, namespace_values: Vec<Value>) -> Result<(), RunError> {\n        let call_position = self.current_position();\n        let func = self.interns.get_function(func_id);\n        let locals_count = u16::try_from(namespace_values.len()).expect(\"coroutine namespace size exceeds u16\");\n\n        // Track memory for the locals\n        let size = namespace_values.len() * std::mem::size_of::<Value>();\n        self.heap.tracker_mut().on_allocate(|| size)?;\n\n        // Extend the stack with the coroutine's pre-bound locals\n        let stack_base = self.stack.len();\n        self.stack.extend(namespace_values);\n\n        // Push frame to execute the coroutine\n        self.push_frame(CallFrame::new_function(\n            &func.code,\n            stack_base,\n            locals_count,\n            func_id,\n            Some(call_position),\n        ))?;\n\n        Ok(())\n    }\n\n    /// Attempts to switch to the next ready task or yields if all tasks are blocked.\n    ///\n    /// This method is called when the current task blocks (e.g., awaiting an unresolved\n    /// future or gather). It performs task context switching:\n    /// 1. Saves current VM context to the current task in the scheduler\n    /// 2. Gets the next ready task from the scheduler\n    /// 3. Loads that task's context into the VM (or initializes a new task from its coroutine)\n    ///\n    /// Returns `Yield(pending_calls)` if no ready tasks (all blocked), or continues\n    /// the run loop if a task was switched to.\n    fn switch_or_yield(&mut self) -> Result<AwaitResult, RunError> {\n        if let Some(next_task_id) = self.scheduler.next_ready_task() {\n            // Save current task context ONLY when switching to another task.\n            // This is critical: if we're about to yield (no ready tasks), the main task's\n            // frames must stay in the VM so they're included in the snapshot.\n            if let Some(current_task_id) = self.scheduler.current_task_id() {\n                self.save_task_context(current_task_id);\n            }\n\n            self.scheduler.set_current_task(Some(next_task_id));\n\n            // Load or initialize the next task's context\n            self.load_or_init_task(next_task_id)?;\n\n            // Continue execution - return FramePushed to reload cache and continue run loop\n            Ok(AwaitResult::FramePushed)\n        } else {\n            // No ready tasks - yield control to host.\n            // Don't save the main task's context - frames stay in VM for the snapshot.\n            Ok(AwaitResult::Yield(self.scheduler.pending_call_ids()))\n        }\n    }\n\n    /// Handles completion of a spawned task.\n    ///\n    /// Called when a spawned task's coroutine returns. This:\n    /// 1. Marks the task as completed in the scheduler\n    /// 2. If the task belongs to a gather, stores the result and checks if gather is complete\n    /// 3. If gather is complete, unblocks the waiter and provides the collected results\n    /// 4. Otherwise, switches to the next ready task\n    pub(super) fn handle_task_completion(&mut self, result: Value) -> Result<AwaitResult, RunError> {\n        let task_id = self\n            .scheduler\n            .current_task_id()\n            .expect(\"handle_task_completion called without current task\");\n        let task = self.scheduler.get_task(task_id);\n        let gather_id = task.gather_id;\n        let gather_result_idx = task.gather_result_idx;\n        let coroutine_id = task.coroutine_id;\n\n        // Mark coroutine as completed\n        if let Some(coro_id) = coroutine_id\n            && let HeapDataMut::Coroutine(coro) = self.heap.get_mut(coro_id)\n        {\n            coro.state = CoroutineState::Completed;\n        }\n\n        // Mark task as completed and store result in task state\n        let task_result = result.clone_with_heap(self.heap);\n        self.scheduler.complete_task(task_id, task_result);\n\n        // If task belongs to a gather, store result and check if gather is complete\n        if let Some(gid) = gather_id {\n            // Store result in gather.results at the correct index\n            if let Some(idx) = gather_result_idx\n                && let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gid)\n            {\n                gather.results[idx] = Some(result);\n            } else {\n                result.drop_with_heap(self.heap);\n            }\n\n            // Extract gather metadata - clone task_ids since we need to check completion\n            // but gather might not be complete yet. We only take task_ids later when\n            // we know gather is complete and will be destroyed.\n            let (task_ids, waiter, pending_calls_empty) = if let HeapData::GatherFuture(gather) = self.heap.get(gid) {\n                (gather.task_ids.clone(), gather.waiter, gather.pending_calls.is_empty())\n            } else {\n                (vec![], None, true)\n            };\n\n            // Check if all tasks are complete AND all external futures are resolved\n            let all_tasks_complete = task_ids.iter().all(|tid| {\n                matches!(\n                    self.scheduler.get_task(*tid).state,\n                    TaskState::Completed(_) | TaskState::Failed(_)\n                )\n            });\n            let all_complete = all_tasks_complete && pending_calls_empty;\n\n            if all_complete {\n                // First check if any task failed\n                let failed_task = task_ids\n                    .iter()\n                    .find(|tid| matches!(self.scheduler.get_task(**tid).state, TaskState::Failed(_)));\n\n                if let Some(&failed_tid) = failed_task {\n                    // Get the error from the failed task\n                    let task = self.scheduler.get_task_mut(failed_tid);\n                    if let TaskState::Failed(err) = std::mem::replace(&mut task.state, TaskState::Ready) {\n                        self.heap.dec_ref(gid);\n\n                        // Switch to waiter so error is raised in its context\n                        if let Some(waiter_id) = waiter {\n                            self.cleanup_current_task();\n                            self.scheduler.set_current_task(Some(waiter_id));\n                            self.load_or_init_task(waiter_id)?;\n                        }\n\n                        return Err(err);\n                    }\n                }\n\n                // Steal results from gather using mem::take - avoids refcount dance\n                // (copy + inc_ref + dec_ref on gather drop). Since gather is being\n                // destroyed, we can take ownership of the values directly.\n                let results: Vec<Value> = if let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gid) {\n                    std::mem::take(&mut gather.results)\n                        .into_iter()\n                        .map(|r| r.expect(\"all results should be filled when gather is complete\"))\n                        .collect()\n                } else {\n                    vec![]\n                };\n\n                // Create result list\n                let list_id = self.heap.allocate(HeapData::List(List::new(results)))?;\n\n                // Release the GatherFuture - this will cascade to release coroutines\n                self.heap.dec_ref(gid);\n\n                // Unblock waiter and switch to it\n                if let Some(waiter_id) = waiter {\n                    self.scheduler.make_ready(waiter_id);\n                    // Remove from ready queue since we're switching directly to it\n                    self.scheduler.remove_from_ready_queue(waiter_id);\n                    // Clear current task's state since it's done\n                    self.cleanup_current_task();\n                    // Switch to waiter\n                    self.scheduler.set_current_task(Some(waiter_id));\n                    self.load_or_init_task(waiter_id)?;\n                    // Push the result onto the waiter's stack\n                    self.push(Value::Ref(list_id));\n                    return Ok(AwaitResult::FramePushed);\n                }\n\n                // No waiter (shouldn't happen but handle gracefully)\n                return Ok(AwaitResult::ValueReady(Value::Ref(list_id)));\n            }\n        } else {\n            // Drop the result (it's stored in the task state now)\n            result.drop_with_heap(self);\n        }\n\n        // Gather not complete or no gather - switch to next task\n        self.cleanup_current_task();\n        self.scheduler.set_current_task(None);\n        if let Some(next_task_id) = self.scheduler.next_ready_task() {\n            self.scheduler.set_current_task(Some(next_task_id));\n            self.load_or_init_task(next_task_id)?;\n            Ok(AwaitResult::FramePushed)\n        } else {\n            Ok(AwaitResult::Yield(self.scheduler.pending_call_ids()))\n        }\n    }\n\n    /// Returns true if the current task is a spawned task (not main).\n    ///\n    /// Used by exception handling to determine if an unhandled exception\n    /// should fail the task rather than propagate out.\n    #[inline]\n    pub(super) fn is_spawned_task(&self) -> bool {\n        self.scheduler.current_task_id().is_some_and(|id| !id.is_main())\n    }\n\n    /// Handles failure of a spawned task due to an unhandled exception.\n    ///\n    /// Called when an exception escapes all frames in a spawned task. This:\n    /// 1. Marks the task as failed in the scheduler\n    /// 2. If the task belongs to a gather, cleans up and propagates to waiter\n    /// 3. Otherwise, switches to the next ready task\n    ///\n    /// # Returns\n    /// - `Ok(())` - Switched to next task, continue execution\n    /// - `Err(error)` - Switched to waiter, handle error in waiter's context\n    ///\n    /// # Panics\n    /// Panics if called for the main task.\n    pub(super) fn handle_task_failure(&mut self, error: RunError) -> Result<(), RunError> {\n        let task_id = self\n            .scheduler\n            .current_task_id()\n            .expect(\"handle_task_failure called without current task\");\n        debug_assert!(!task_id.is_main(), \"handle_task_failure called for main task\");\n\n        // Get task's gather_id before marking failed\n        let gather_id = self.scheduler.get_task(task_id).gather_id;\n\n        // If part of a gather, propagate error to waiter\n        if let Some(gid) = gather_id {\n            // Get waiter and take task_ids from GatherFuture - gather is being destroyed anyway\n            let (waiter, task_ids) = if let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gid) {\n                (gather.waiter, std::mem::take(&mut gather.task_ids))\n            } else {\n                (None, vec![])\n            };\n\n            // Mark task as failed\n            self.scheduler.fail_task(task_id, error);\n\n            // Cancel sibling tasks (filter out self and already-finished tasks inline)\n            for sibling_id in task_ids {\n                if sibling_id != task_id && !self.scheduler.get_task(sibling_id).is_finished() {\n                    self.scheduler.cancel_task(sibling_id, self.heap);\n                }\n            }\n\n            // Clean up the gather\n            self.heap.dec_ref(gid);\n\n            // Switch to waiter and propagate the error\n            if let Some(waiter_id) = waiter {\n                self.cleanup_current_task();\n                self.scheduler.set_current_task(Some(waiter_id));\n                self.load_or_init_task(waiter_id)?;\n                // Get error back from task state to return\n                let task = self.scheduler.get_task_mut(task_id);\n                if let TaskState::Failed(err) = std::mem::replace(&mut task.state, TaskState::Ready) {\n                    return Err(err);\n                }\n            }\n        } else {\n            // No gather - just mark task as failed (ignore returned gather_id which is None)\n            let _ = self.scheduler.fail_task(task_id, error);\n        }\n\n        // No gather or no waiter - switch to next task\n        self.cleanup_current_task();\n        self.scheduler.set_current_task(None);\n        if let Some(next_task_id) = self.scheduler.next_ready_task() {\n            self.scheduler.set_current_task(Some(next_task_id));\n            self.load_or_init_task(next_task_id)?;\n        }\n        // If no ready tasks, frames will be empty and run loop will yield\n\n        Ok(())\n    }\n\n    /// Saves the current VM context into the given task in the scheduler.\n    ///\n    /// Serializes frames, moves stack/exception_stack, stores instruction_ip,\n    /// and adjusts the global recursion depth counter.\n    fn save_task_context(&mut self, task_id: TaskId) {\n        let frames: Vec<SerializedTaskFrame> = self\n            .frames\n            .drain(..)\n            .map(|f| SerializedTaskFrame {\n                function_id: f.function_id,\n                ip: f.ip,\n                stack_base: f.stack_base,\n                locals_count: f.locals_count,\n                call_position: f.call_position,\n            })\n            .collect();\n\n        // Count this task's recursion depth contribution and subtract it from\n        // the global counter so the next task gets a clean budget.\n        let task_depth = frames.len().saturating_sub(1); // root frame doesn't contribute to recursion depth\n        let global_depth = self.heap.get_recursion_depth();\n        self.heap.set_recursion_depth(global_depth - task_depth);\n\n        // Save VM state into the task\n        let task = self.scheduler.get_task_mut(task_id);\n        task.frames = frames;\n        task.stack = std::mem::take(&mut self.stack);\n        task.exception_stack = std::mem::take(&mut self.exception_stack);\n        task.instruction_ip = self.instruction_ip;\n    }\n\n    /// Loads an existing task's context or initializes a new task from its coroutine.\n    ///\n    /// If the task has stored frames, restores them into the VM. If the task was\n    /// unblocked by an external future resolution, pushes the resolved value onto\n    /// the restored stack so execution can continue past the AWAIT opcode.\n    /// If the task has a coroutine_id but no frames, starts the coroutine.\n    ///\n    /// Restores the task's recursion depth contribution to the global counter\n    /// (balances the subtraction in `save_task_context`).\n    fn load_or_init_task(&mut self, task_id: TaskId) -> Result<(), RunError> {\n        let task = self.scheduler.get_task_mut(task_id);\n        let frames = std::mem::take(&mut task.frames);\n        let stack = std::mem::take(&mut task.stack);\n        let exception_stack = std::mem::take(&mut task.exception_stack);\n        let instruction_ip = task.instruction_ip;\n        let coroutine_id = task.coroutine_id;\n\n        // Restore this task's recursion depth contribution to the global counter\n        let task_depth = frames.len().saturating_sub(1); // root frame doesn't contribute to recursion depth\n        let global_depth = self.heap.get_recursion_depth();\n        self.heap.set_recursion_depth(global_depth + task_depth);\n\n        if !frames.is_empty() {\n            // Task has existing context - restore it\n            self.stack = stack;\n            self.exception_stack = exception_stack;\n            self.instruction_ip = instruction_ip;\n\n            // Reconstruct CallFrames from serialized form\n            self.frames = frames\n                .into_iter()\n                .map(|sf| {\n                    let code = match sf.function_id {\n                        Some(func_id) => &self.interns.get_function(func_id).code,\n                        None => {\n                            // This happens for the main task's module-level code\n                            self.module_code.expect(\"module_code not set for main task frame\")\n                        }\n                    };\n                    CallFrame {\n                        code,\n                        ip: sf.ip,\n                        stack_base: sf.stack_base,\n                        locals_count: sf.locals_count,\n                        function_id: sf.function_id,\n                        call_position: sf.call_position,\n                        should_return: false,\n                    }\n                })\n                .collect();\n        } else if let Some(coro_id) = coroutine_id {\n            // New task - start from coroutine\n            self.init_task_from_coroutine(coro_id)?;\n        } else {\n            // This shouldn't happen - task with no frames and no coroutine\n            panic!(\"task has no frames and no coroutine_id\");\n        }\n\n        // If this task was unblocked by a resolved external future, push the\n        // resolved value onto the stack. The AWAIT opcode already advanced the IP\n        // past itself before the task was saved, so execution will continue with\n        // the resolved value on top of the stack.\n        if let Some(value) = self.scheduler.take_resolved_for_task(task_id) {\n            self.push(value);\n        }\n\n        Ok(())\n    }\n\n    /// Initializes the VM state to run a coroutine for a spawned task.\n    ///\n    /// Similar to exec_get_awaitable's coroutine handling, but for task initialization.\n    fn init_task_from_coroutine(&mut self, coroutine_id: HeapId) -> Result<(), RunError> {\n        // Get coroutine data\n        let heap_data = self.heap.get(coroutine_id);\n        let HeapData::Coroutine(coro) = heap_data else {\n            panic!(\"task coroutine_id doesn't point to a Coroutine\")\n        };\n\n        // Check state\n        if coro.state != CoroutineState::New {\n            return Err(\n                SimpleException::new_msg(ExcType::RuntimeError, \"cannot reuse already awaited coroutine\").into(),\n            );\n        }\n\n        // Extract coroutine data\n        let func_id = coro.func_id;\n        let namespace_values: Vec<Value> = coro.namespace.iter().map(|v| v.clone_with_heap(self)).collect();\n\n        // Mark coroutine as Running\n        if let HeapDataMut::Coroutine(coro_mut) = self.heap.get_mut(coroutine_id) {\n            coro_mut.state = CoroutineState::Running;\n        }\n\n        // Push locals onto stack and push frame directly (can't use start_coroutine_frame\n        // because that needs a current frame for call_position, but spawned tasks\n        // don't have a parent frame — the coroutine is the root)\n        let func = self.interns.get_function(func_id);\n        let locals_count = u16::try_from(namespace_values.len()).expect(\"coroutine namespace size exceeds u16\");\n\n        // Track memory for the locals\n        let size = namespace_values.len() * std::mem::size_of::<Value>();\n        self.heap.tracker_mut().on_allocate(|| size)?;\n\n        let stack_base = self.stack.len();\n        self.stack.extend(namespace_values);\n\n        self.push_frame(CallFrame::new_function(\n            &func.code,\n            stack_base,\n            locals_count,\n            func_id,\n            None, // No call position — this is the root frame for a spawned task\n        ))?;\n\n        Ok(())\n    }\n\n    /// Resolves an external future with a value.\n    ///\n    /// Called by the host when an async external call completes.\n    /// Stores the result in the scheduler, which will unblock any task\n    /// waiting on this CallId.\n    ///\n    /// If the task that created this call has been cancelled or failed,\n    /// the result is silently ignored and the value is dropped.\n    pub fn resolve_future(&mut self, call_id: u32, obj: MontyObject) -> Result<(), InvalidInputError> {\n        let call_id = CallId::new(call_id);\n        // Check if the creator task has been cancelled/failed\n        if let Some(creator_task) = self.scheduler.get_pending_call_creator(call_id)\n            && self.scheduler.is_task_failed(creator_task)\n        {\n            // Task was cancelled - silently ignore the result\n            return Ok(());\n        }\n        let value = obj.to_value(self)?;\n\n        // Check if a gather is waiting on this CallId\n        if let Some((gather_id, result_idx)) = self.scheduler.take_gather_waiter(call_id) {\n            self.scheduler.remove_pending_call(call_id);\n\n            // Store result directly in gather (move, not clone) and check completion\n            let (pending_empty, task_ids, waiter) =\n                if let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gather_id) {\n                    gather.results[result_idx] = Some(value); // Move value directly, no clone needed\n                    // Remove from pending_calls\n                    gather.pending_calls.retain(|&cid| cid != call_id);\n                    // Take task_ids to avoid clone - we're checking completion so gather may be destroyed\n                    (\n                        gather.pending_calls.is_empty(),\n                        std::mem::take(&mut gather.task_ids),\n                        gather.waiter,\n                    )\n                } else {\n                    (true, vec![], None)\n                };\n\n            // Check if gather is now complete (all external futures resolved and all tasks complete)\n            if pending_empty {\n                let all_tasks_complete = task_ids.is_empty()\n                    || task_ids.iter().all(|tid| {\n                        matches!(\n                            self.scheduler.get_task(*tid).state,\n                            TaskState::Completed(_) | TaskState::Failed(_)\n                        )\n                    });\n                if all_tasks_complete {\n                    // Gather is complete - build result and push to waiter's stack\n                    if let Some(waiter_id) = waiter {\n                        // Steal results from gather using mem::take - avoids refcount dance\n                        // (copy + inc_ref + dec_ref on gather drop). Since gather is being\n                        // destroyed, we can take ownership of the values directly.\n                        let results: Vec<Value> =\n                            if let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gather_id) {\n                                std::mem::take(&mut gather.results)\n                                    .into_iter()\n                                    .map(|r| r.expect(\"all results should be filled when gather is complete\"))\n                                    .collect()\n                            } else {\n                                vec![]\n                            };\n\n                        // Create result list - if this fails, we can't do much, just skip\n                        if let Ok(list_id) = self.heap.allocate(HeapData::List(List::new(results))) {\n                            // Release the GatherFuture (results already taken, so no double-drop)\n                            self.heap.dec_ref(gather_id);\n\n                            // Push result onto waiter's stack and mark as ready.\n                            // Check if the waiter's context is currently in the VM (frames not saved\n                            // to the task). This is the case when the waiter is the current task\n                            // and hasn't been switched away from (e.g., external-only gather).\n                            let waiter_context_in_vm =\n                                self.scheduler.current_task_id() == Some(waiter_id) && !self.frames.is_empty();\n\n                            if waiter_context_in_vm {\n                                // Waiter's frames are in the VM - push directly onto VM stack\n                                self.stack.push(Value::Ref(list_id));\n                                // Mark as ready but don't add to ready_queue\n                                self.scheduler.get_task_mut(waiter_id).state = TaskState::Ready;\n                            } else {\n                                // Waiter's context is saved in the task (either spawned task,\n                                // or main task that was saved when switching to spawned tasks)\n                                self.scheduler.get_task_mut(waiter_id).stack.push(Value::Ref(list_id));\n                                self.scheduler.make_ready(waiter_id);\n                            }\n                        }\n                    }\n                }\n            }\n        } else {\n            // Normal resolution for single awaiter\n            self.scheduler.resolve(call_id, value);\n        }\n        Ok(())\n    }\n\n    /// Fails an external future with an error.\n    ///\n    /// Called by the host when an async external call fails with an exception.\n    /// Finds the task blocked on this CallId and fails it with the error.\n    /// If the task is part of a gather, cancels sibling tasks.\n    pub fn fail_future(&mut self, call_id: u32, error: RunError) {\n        let call_id = CallId::new(call_id);\n\n        // Check if a gather is waiting on this CallId\n        if let Some((gather_id, _result_idx)) = self.scheduler.take_gather_waiter(call_id) {\n            // Remove from pending_calls so it doesn't appear in get_pending_call_ids()\n            // (fail_for_call handles this for the non-gather case)\n            self.scheduler.remove_pending_call(call_id);\n\n            // Get the gather's waiter, task_ids, and OTHER pending calls\n            // We need to remove all pending calls for this gather from gather_waiters\n            // before we dec_ref the gather, otherwise subsequent errors for the same\n            // gather would try to access a freed heap object.\n            // Use get_mut and take to avoid allocations - gather is being destroyed anyway.\n            let (waiter, task_ids, other_pending_calls) =\n                if let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gather_id) {\n                    let mut other_calls = std::mem::take(&mut gather.pending_calls);\n                    other_calls.retain(|&cid| cid != call_id);\n                    (gather.waiter, std::mem::take(&mut gather.task_ids), other_calls)\n                } else {\n                    (None, vec![], vec![])\n                };\n\n            // Remove all other pending calls for this gather from gather_waiters and pending_calls\n            // This prevents subsequent errors from trying to access the freed gather\n            for other_call_id in other_pending_calls {\n                self.scheduler.take_gather_waiter(other_call_id);\n                self.scheduler.remove_pending_call(other_call_id);\n            }\n\n            // Cancel all sibling tasks in the gather\n            for sibling_id in task_ids {\n                self.scheduler.cancel_task(sibling_id, self.heap);\n            }\n\n            // Fail the waiter task (the task that awaited the gather)\n            if let Some(waiter_id) = waiter {\n                self.scheduler.fail_task(waiter_id, error);\n                // Release the GatherFuture\n                self.heap.dec_ref(gather_id);\n            }\n        } else if let Some((task_id, Some(gid))) = self.scheduler.fail_for_call(call_id, error) {\n            // Original path: task is directly BlockedOnCall and part of a gather\n            // Take task_ids from GatherFuture - gather is being destroyed anyway\n            let task_ids: Vec<TaskId> = if let HeapDataMut::GatherFuture(gather) = self.heap.get_mut(gid) {\n                std::mem::take(&mut gather.task_ids)\n            } else {\n                vec![]\n            };\n\n            // Cancel sibling tasks (filter out self and already-finished tasks)\n            for sibling_id in task_ids {\n                if sibling_id != task_id && !self.scheduler.get_task(sibling_id).is_finished() {\n                    self.scheduler.cancel_task(sibling_id, self.heap);\n                }\n            }\n        }\n    }\n\n    /// Adds pending call data for an external function call.\n    ///\n    /// Called by `run_pending()` when the host chooses async resolution.\n    /// This stores the call data in the scheduler so we can:\n    /// 1. Track which task created the call (to ignore results if cancelled)\n    /// 2. Return pending call info when all tasks are blocked\n    ///\n    /// Note: The args are empty because the host already has them from the\n    /// `FunctionCall` return value. We only need to track the creator task.\n    pub fn add_pending_call(&mut self, call_id: CallId) {\n        let current_task = self.scheduler.current_task_id().unwrap_or_default();\n        self.scheduler.add_pending_call(\n            call_id,\n            PendingCallData {\n                args: ArgValues::Empty,\n                creator_task: current_task,\n            },\n        );\n    }\n\n    /// Prepares the current task to continue after futures are resolved.\n    ///\n    /// When the current task (main or spawned) was blocked on an external future and\n    /// that future is now resolved, this method takes the resolved value from the\n    /// scheduler and pushes it onto the VM's stack so execution can continue.\n    ///\n    /// This is called by `FutureSnapshot::resume()` after resolving futures but before\n    /// calling `vm.run()`. It handles the task whose frames are currently in the VM.\n    /// Other unblocked tasks get their resolved values during task switching in\n    /// `load_or_init_task`.\n    ///\n    /// # Returns\n    /// `true` if a value was pushed, `false` if no task was ready to continue.\n    pub fn prepare_current_task_after_resolve(&mut self) -> bool {\n        // Check if there's a current task (main or spawned)\n        let Some(current_task_id) = self.scheduler.current_task_id() else {\n            return false;\n        };\n\n        // Take the resolved value for the current task (if it was unblocked)\n        if let Some(value) = self.scheduler.take_resolved_for_task(current_task_id) {\n            // Remove task from ready_queue since we're handling it directly.\n            // resolve() added it to ready_queue, but since frames are already\n            // in the VM (not saved/restored), we handle it here instead of via task switching.\n            self.scheduler.remove_from_ready_queue(current_task_id);\n            self.push(value);\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Loads a ready task if the VM needs one.\n    ///\n    /// This is called by `FutureSnapshot::resume()` after resolving futures but before\n    /// calling `vm.run()`. It handles two cases:\n    /// 1. **No frames in VM**: A task context needs to be loaded from the scheduler\n    ///    (e.g., gather completed while tasks were running and we yielded with no frames).\n    /// 2. **Current task is blocked**: The current task's frames are in the VM but it's\n    ///    still blocked (e.g., only some futures were resolved in incremental resolution).\n    ///    Saves the blocked task's context and switches to a ready task.\n    ///\n    /// # Returns\n    /// - `Ok(true)` if a task was loaded and execution can continue\n    /// - `Ok(false)` if no task switch is needed (current task is runnable or no ready tasks)\n    /// - `Err(error)` if loading the task failed\n    pub fn load_ready_task_if_needed(&mut self) -> Result<bool, RunError> {\n        // If frames exist, check if the current task is blocked. If it's not blocked\n        // (i.e., it was just unblocked), there's nothing to do - it will continue running.\n        if !self.frames.is_empty() {\n            let current_blocked = self.scheduler.current_task_id().is_some_and(|tid| {\n                matches!(\n                    self.scheduler.get_task(tid).state,\n                    TaskState::BlockedOnCall(_) | TaskState::BlockedOnGather(_)\n                )\n            });\n            if !current_blocked {\n                return Ok(false);\n            }\n\n            // Current task is blocked - save its context before switching\n            if let Some(tid) = self.scheduler.current_task_id() {\n                self.save_task_context(tid);\n            }\n        }\n\n        // Check if there's a ready task to load\n        let Some(next_task_id) = self.scheduler.next_ready_task() else {\n            return Ok(false);\n        };\n\n        self.scheduler.set_current_task(Some(next_task_id));\n        self.load_or_init_task(next_task_id)?;\n        Ok(true)\n    }\n\n    /// Gets the pending call IDs from the scheduler.\n    pub fn get_pending_call_ids(&self) -> Vec<CallId> {\n        self.scheduler.pending_call_ids()\n    }\n\n    /// Takes the error from a failed task if the current task has failed.\n    ///\n    /// Returns `Some(error)` if the current task is in `TaskState::Failed`, `None` otherwise.\n    /// Used by `FutureSnapshot::resume` to propagate errors after resolving futures.\n    ///\n    /// Only replaces the state when the task has actually failed - other states\n    /// (e.g., `BlockedOnCall`) are left untouched.\n    pub fn take_failed_task_error(&mut self) -> Option<RunError> {\n        let current_task_id = self.scheduler.current_task_id()?;\n        let task = self.scheduler.get_task_mut(current_task_id);\n\n        // Only replace state if it's actually Failed - otherwise we'd corrupt\n        // the task's real state (e.g., BlockedOnCall) by overwriting it with Ready.\n        if matches!(task.state, TaskState::Failed(_))\n            && let TaskState::Failed(error) = std::mem::replace(&mut task.state, TaskState::Ready)\n        {\n            return Some(error);\n        }\n        None\n    }\n}\n\n/// Internal enum for dispatching await operations by heap data type.\n///\n/// Used in `exec_get_awaitable` to determine which handler to call after\n/// inspecting the heap data type. This avoids borrow conflicts between\n/// the heap reference and `&mut self` needed by the handler methods.\nenum AwaitableType {\n    Coroutine,\n    GatherFuture,\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/attr.rs",
    "content": "//! Attribute access helpers for the VM.\n\nuse super::VM;\nuse crate::{\n    bytecode::vm::CallResult,\n    defer_drop,\n    exception_private::{ExcType, RunError},\n    intern::StringId,\n    resource::ResourceTracker,\n    value::EitherStr,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Loads an attribute from an object and pushes it onto the stack.\n    ///\n    /// Returns an AttributeError if the attribute doesn't exist.\n    pub(super) fn load_attr(&mut self, name_id: StringId) -> Result<CallResult, RunError> {\n        let this = self;\n\n        let obj = this.pop();\n        defer_drop!(obj, this);\n\n        let attr = EitherStr::Interned(name_id);\n        obj.py_getattr(&attr, this)\n    }\n\n    /// Loads an attribute from a module for `from ... import` and pushes it onto the stack.\n    ///\n    /// Returns an ImportError (not AttributeError) if the attribute doesn't exist,\n    /// matching CPython's behavior for `from module import name`.\n    pub(super) fn load_attr_import(&mut self, name_id: StringId) -> Result<CallResult, RunError> {\n        let this = self;\n\n        let obj = this.pop();\n        defer_drop!(obj, this);\n\n        let attr = EitherStr::Interned(name_id);\n        match obj.py_getattr(&attr, this) {\n            Ok(result) => Ok(result),\n            Err(RunError::Exc(exc)) if exc.exc.exc_type() == ExcType::AttributeError => {\n                // Only compute module_name when we need it for the error message\n                let module_name = obj.module_name(this.heap, this.interns);\n                let name_str = this.interns.get_str(name_id);\n                Err(ExcType::cannot_import_name(name_str, &module_name))\n            }\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Stores a value as an attribute on an object.\n    ///\n    /// Returns an AttributeError if the attribute cannot be set.\n    pub(super) fn store_attr(&mut self, name_id: StringId) -> Result<(), RunError> {\n        let this = self;\n\n        let obj = this.pop();\n        defer_drop!(obj, this);\n\n        let value = this.pop();\n        // py_set_attr takes ownership of value and drops it on error\n        obj.py_set_attr(name_id, value, this)\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/binary.rs",
    "content": "//! Binary and in-place operation helpers for the VM.\n\nuse super::VM;\nuse crate::{\n    defer_drop,\n    exception_private::{ExcType, RunError},\n    heap::{Heap, HeapData, HeapGuard},\n    resource::ResourceTracker,\n    types::{PyTrait, Set, dict_view::collect_iterable_to_set, set::SetBinaryOp},\n    value::BitwiseOp,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Binary addition with proper refcount handling.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths to avoid\n    /// overhead on the success path (99%+ of operations).\n    pub(super) fn binary_add(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        match lhs.py_add(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"+\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e.into()),\n        }\n    }\n\n    /// Binary subtraction with proper refcount handling.\n    ///\n    /// Handles both numeric subtraction and set difference (`-` operator).\n    /// For sets/frozensets, delegates to [`binary_set_op`] which needs `interns`\n    /// for element hashing and equality. Uses lazy type capture: only calls\n    /// `py_type()` in error paths.\n    pub(super) fn binary_sub(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        if let Some(result) = this.binary_dict_view_op(lhs, rhs, DictViewBinaryOp::Sub)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        if let Some(result) = this.binary_set_op(lhs, rhs, SetBinaryOp::Sub)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        match lhs.py_sub(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"-\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e.into()),\n        }\n    }\n\n    /// Binary multiplication with proper refcount handling.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    pub(super) fn binary_mult(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        match lhs.py_mult(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"*\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Binary division with proper refcount handling.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    pub(super) fn binary_div(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        match lhs.py_div(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"/\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Binary floor division with proper refcount handling.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    pub(super) fn binary_floordiv(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        match lhs.py_floordiv(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"//\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Binary modulo with proper refcount handling.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    pub(super) fn binary_mod(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        match lhs.py_mod(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"%\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Binary power with proper refcount handling.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    #[inline(never)]\n    pub(super) fn binary_pow(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        match lhs.py_pow(rhs, this) {\n            Ok(Some(v)) => {\n                this.push(v);\n                Ok(())\n            }\n            Ok(None) => {\n                let lhs_type = lhs.py_type(this.heap);\n                let rhs_type = rhs.py_type(this.heap);\n                Err(ExcType::binary_type_error(\"** or pow()\", lhs_type, rhs_type))\n            }\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Binary bitwise operation on integers and sets.\n    ///\n    /// For integers, performs standard bitwise operations (AND, OR, XOR, shifts).\n    /// For sets/frozensets, `|` maps to union, `&` to intersection, and `^` to\n    /// symmetric difference. Set operations are handled here because `py_bitwise`\n    /// doesn't have access to `interns`, which set operations need for hashing.\n    pub(super) fn binary_bitwise(&mut self, op: BitwiseOp) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        // Set/frozenset operations: |, &, ^ map to union, intersection,\n        // symmetric_difference. Shifts don't apply to sets.\n        let set_op = match op {\n            BitwiseOp::Or => Some(SetBinaryOp::Or),\n            BitwiseOp::And => Some(SetBinaryOp::And),\n            BitwiseOp::Xor => Some(SetBinaryOp::Xor),\n            BitwiseOp::LShift | BitwiseOp::RShift => None,\n        };\n        if let Some(set_op) = set_op\n            && let Some(result) = this.binary_set_op(lhs, rhs, set_op)?\n        {\n            this.push(result);\n            return Ok(());\n        }\n\n        let result = lhs.py_bitwise(rhs, op, this.heap)?;\n        this.push(result);\n        Ok(())\n    }\n\n    /// Binary `&` with CPython-style dict-keys special handling before numeric fallback.\n    ///\n    /// Milestone one only needs one non-numeric behavior here: `dict_keys & iterable`\n    /// should iterate the right-hand side, return a plain `set`, and raise\n    /// `TypeError(\"'X' object is not iterable\")` for non-iterable operands.\n    pub(super) fn binary_and(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        if let Some(result) = this.binary_dict_view_op(lhs, rhs, DictViewBinaryOp::And)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        if let Some(result) = this.binary_set_op(lhs, rhs, SetBinaryOp::And)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        let result = lhs.py_bitwise(rhs, BitwiseOp::And, this.heap)?;\n        this.push(result);\n        Ok(())\n    }\n\n    /// Binary `|` with CPython-style dict-view handling before numeric fallback.\n    pub(super) fn binary_or(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        if let Some(result) = this.binary_dict_view_op(lhs, rhs, DictViewBinaryOp::Or)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        if let Some(result) = this.binary_set_op(lhs, rhs, SetBinaryOp::Or)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        let result = lhs.py_bitwise(rhs, BitwiseOp::Or, this.heap)?;\n        this.push(result);\n        Ok(())\n    }\n\n    /// Binary `^` with CPython-style dict-view handling before numeric fallback.\n    pub(super) fn binary_xor(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        if let Some(result) = this.binary_dict_view_op(lhs, rhs, DictViewBinaryOp::Xor)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        if let Some(result) = this.binary_set_op(lhs, rhs, SetBinaryOp::Xor)? {\n            this.push(result);\n            return Ok(());\n        }\n\n        let result = lhs.py_bitwise(rhs, BitwiseOp::Xor, this.heap)?;\n        this.push(result);\n        Ok(())\n    }\n\n    /// In-place addition (uses py_iadd for mutable containers, falls back to py_add).\n    ///\n    /// For mutable types like lists, `py_iadd` mutates in place and returns true.\n    /// For immutable types, we fall back to regular addition.\n    ///\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    ///\n    /// Note: Cannot use `defer_drop!` for `lhs` here because on successful in-place\n    /// operation, we need to push `lhs` back onto the stack rather than drop it.\n    pub(super) fn inplace_add(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        // Use HeapGuard because inplace addition will push lhs back on the stack if successful\n        let mut lhs_guard = HeapGuard::new(this.pop(), this);\n        let (lhs, this) = lhs_guard.as_parts_mut();\n\n        // Try in-place operation first (for mutable types like lists)\n        if lhs.py_iadd(rhs, this, lhs.ref_id())? {\n            // In-place operation succeeded - push lhs back\n            let (lhs, this) = lhs_guard.into_parts();\n            this.push(lhs);\n            return Ok(());\n        }\n\n        // Next try regular addition\n        if let Some(v) = lhs.py_add(rhs, this)? {\n            this.push(v);\n            return Ok(());\n        }\n\n        let lhs_type = lhs.py_type(this.heap);\n        let rhs_type = rhs.py_type(this.heap);\n        Err(ExcType::binary_type_error(\"+=\", lhs_type, rhs_type))\n    }\n\n    /// Binary matrix multiplication (`@` operator).\n    ///\n    /// Currently not implemented - returns a `NotImplementedError`.\n    /// Matrix multiplication requires numpy-like array types which Monty doesn't support.\n    pub(super) fn binary_matmul(&mut self) -> Result<(), RunError> {\n        let rhs = self.pop();\n        let lhs = self.pop();\n        lhs.drop_with_heap(self);\n        rhs.drop_with_heap(self);\n        Err(ExcType::not_implemented(\"matrix multiplication (@) is not supported\").into())\n    }\n\n    /// Implements dict-view set-like operators before falling back to other dispatch.\n    ///\n    /// Returning `Ok(None)` means the left operand was not a set-like dict view, so the\n    /// caller should continue with ordinary numeric or pure-set dispatch.\n    fn binary_dict_view_op(\n        &mut self,\n        lhs: &crate::value::Value,\n        rhs: &crate::value::Value,\n        op: DictViewBinaryOp,\n    ) -> Result<Option<crate::value::Value>, RunError> {\n        let this = self;\n        let crate::value::Value::Ref(lhs_id) = lhs else {\n            return Ok(None);\n        };\n\n        let lhs_set = match this.heap.get(*lhs_id) {\n            HeapData::DictKeysView(view) => view.to_set(this)?,\n            HeapData::DictItemsView(view) => view.to_set(this)?,\n            _ => return Ok(None),\n        };\n        defer_drop!(lhs_set, this);\n\n        let rhs_set = collect_iterable_to_set(rhs.clone_with_heap(this), this)?;\n        defer_drop!(rhs_set, this);\n\n        let result = apply_dict_view_binary_op(lhs_set, rhs_set, op, this)?;\n\n        let result_id = this.heap.allocate(HeapData::Set(result))?;\n        Ok(Some(crate::value::Value::Ref(result_id)))\n    }\n\n    /// Implements pure set/frozenset binary operators with strict operand checks.\n    ///\n    /// Method forms accept arbitrary iterables, but the operator forms handled here\n    /// must reject non-set operands so Monty matches CPython's `TypeError` behavior.\n    fn binary_set_op(\n        &mut self,\n        lhs: &crate::value::Value,\n        rhs: &crate::value::Value,\n        op: SetBinaryOp,\n    ) -> Result<Option<crate::value::Value>, RunError> {\n        let this = self;\n        let crate::value::Value::Ref(lhs_id) = lhs else {\n            return Ok(None);\n        };\n\n        let result = Heap::with_entry_mut(this, *lhs_id, |this, data| match data {\n            crate::heap_data::HeapDataMut::Set(set) => set.binary_op_value(rhs, op, this).map(|v| v.map(HeapData::Set)),\n            crate::heap_data::HeapDataMut::FrozenSet(set) => {\n                set.binary_op_value(rhs, op, this).map(|v| v.map(HeapData::FrozenSet))\n            }\n            _ => Ok(None),\n        })?;\n\n        let Some(result) = result else {\n            return Ok(None);\n        };\n        let result_id = this.heap.allocate(result)?;\n        Ok(Some(crate::value::Value::Ref(result_id)))\n    }\n}\n\n/// Supported dict-view set-like operators.\n#[derive(Debug, Clone, Copy)]\nenum DictViewBinaryOp {\n    And,\n    Or,\n    Xor,\n    Sub,\n}\n\n/// Applies a set-like operator to two temporary sets and returns a plain `set`.\nfn apply_dict_view_binary_op(\n    lhs: &Set,\n    rhs: &Set,\n    op: DictViewBinaryOp,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<Set, RunError> {\n    let mut result = match op {\n        DictViewBinaryOp::And => Set::with_capacity(lhs.len().min(rhs.len())),\n        DictViewBinaryOp::Or => Set::with_capacity(lhs.len() + rhs.len()),\n        DictViewBinaryOp::Xor => Set::with_capacity(lhs.len() + rhs.len()),\n        DictViewBinaryOp::Sub => Set::with_capacity(lhs.len()),\n    };\n\n    match op {\n        DictViewBinaryOp::And => {\n            let (smaller, larger) = if lhs.len() <= rhs.len() { (lhs, rhs) } else { (rhs, lhs) };\n            for value in smaller.iter() {\n                if larger.contains(value, vm)? {\n                    result.add(value.clone_with_heap(vm), vm)?;\n                }\n            }\n        }\n        DictViewBinaryOp::Or => {\n            for value in lhs.iter() {\n                result.add(value.clone_with_heap(vm), vm)?;\n            }\n            for value in rhs.iter() {\n                result.add(value.clone_with_heap(vm), vm)?;\n            }\n        }\n        DictViewBinaryOp::Xor => {\n            for value in lhs.iter() {\n                if !rhs.contains(value, vm)? {\n                    result.add(value.clone_with_heap(vm), vm)?;\n                }\n            }\n            for value in rhs.iter() {\n                if !lhs.contains(value, vm)? {\n                    result.add(value.clone_with_heap(vm), vm)?;\n                }\n            }\n        }\n        DictViewBinaryOp::Sub => {\n            for value in lhs.iter() {\n                if !rhs.contains(value, vm)? {\n                    result.add(value.clone_with_heap(vm), vm)?;\n                }\n            }\n        }\n    }\n\n    Ok(result)\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/call.rs",
    "content": "//! Function call helpers for the VM.\n//!\n//! This module contains the implementation of call-related opcodes and helper\n//! functions for executing function calls. The main entry points are the `exec_*`\n//! methods which are called from the VM's main dispatch loop.\n\nuse super::{CallFrame, VM};\nuse crate::{\n    args::{ArgValues, KwargsValues},\n    asyncio::Coroutine,\n    builtins::{Builtins, BuiltinsFunctions},\n    bytecode::FrameExit,\n    defer_drop,\n    exception_private::{ExcType, RunError},\n    heap::{DropWithHeap, Heap, HeapData, HeapGuard, HeapId},\n    heap_data::CellValue,\n    intern::{FunctionId, StringId},\n    os::OsFunction,\n    resource::ResourceTracker,\n    types::{Dict, PyTrait, Type, bytes::call_bytes_method, str::call_str_method, r#type::call_type_method},\n    value::{EitherStr, Value},\n};\n\n/// Result of executing a call or attribute method.\n///\n/// Used by the `exec_*` methods and `py_call_attr` implementations to communicate\n/// what action the VM's main loop should take after the call completes.\n///\n/// For attribute methods that complete synchronously, use `CallResult::Value`.\n/// For operations requiring host involvement (OS calls, external functions, etc.),\n/// use the appropriate variant to signal the VM to yield.\npub(crate) enum CallResult {\n    /// Call completed synchronously with a return value.\n    Value(Value),\n    /// A new frame was pushed for a defined function call.\n    /// The VM should reload its cached frame state.\n    FramePushed,\n    /// External function call requested - VM should pause and return to caller.\n    /// The `EitherStr` is the name of the external function (interned or heap-owned).\n    External(EitherStr, ArgValues),\n    /// OS operation call requested - VM should yield `FrameExit::OsCall` to host.\n    ///\n    /// The host executes the OS operation and resumes the VM with the result.\n    OsCall(OsFunction, ArgValues),\n    /// Dataclass method call requested - VM should yield `FrameExit::MethodCall` to host.\n    ///\n    /// The method name (e.g. `\"distance\"`) and the args include the dataclass instance\n    /// as the first argument (`self`). Unlike `External`, this uses an `EitherStr` instead\n    /// of `StringId` because method names are only known at runtime when dataclass\n    /// inputs are provided.\n    MethodCall(EitherStr, ArgValues),\n    /// The call returned a value that should be implicitly awaited.\n    ///\n    /// Used by `asyncio.run()` to execute a coroutine without an explicit `await`.\n    /// The VM will push the value onto the stack and execute `exec_get_awaitable`.\n    AwaitValue(Value),\n}\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    // ========================================================================\n    // Call Opcode Executors\n    // ========================================================================\n    // These methods are called from the VM's main dispatch loop to execute\n    // call-related opcodes. They handle stack operations and return a result\n    // indicating what the VM should do next.\n\n    /// Executes `CallFunction` opcode.\n    ///\n    /// Pops the callable and arguments from the stack, calls the function,\n    /// and returns the result.\n    pub(super) fn exec_call_function(&mut self, arg_count: usize) -> Result<CallResult, RunError> {\n        let args = self.pop_n_args(arg_count);\n        let callable = self.pop();\n        let this = self;\n        defer_drop!(callable, this);\n        this.call_function(callable, args)\n    }\n\n    /// Executes `CallBuiltinFunction` opcode.\n    ///\n    /// Calls a builtin function directly without stack manipulation for the callable.\n    /// This is an optimization that avoids constant pool lookup and stack manipulation.\n    pub(super) fn exec_call_builtin_function(&mut self, builtin_id: u8, arg_count: usize) -> Result<Value, RunError> {\n        // Convert u8 to BuiltinsFunctions via FromRepr\n        if let Some(builtin) = BuiltinsFunctions::from_repr(builtin_id) {\n            let args = self.pop_n_args(arg_count);\n            builtin.call(self, args)\n        } else {\n            Err(RunError::internal(\"CallBuiltinFunction: invalid builtin_id\"))\n        }\n    }\n\n    /// Executes `CallBuiltinType` opcode.\n    ///\n    /// Calls a builtin type constructor directly without stack manipulation for the callable.\n    /// This is an optimization for type constructors like `list()`, `int()`, `str()`.\n    pub(super) fn exec_call_builtin_type(&mut self, type_id: u8, arg_count: usize) -> Result<Value, RunError> {\n        // Convert u8 to Type via callable_from_u8\n        if let Some(t) = Type::callable_from_u8(type_id) {\n            let args = self.pop_n_args(arg_count);\n            t.call(self, args)\n        } else {\n            Err(RunError::internal(\"CallBuiltinType: invalid type_id\"))\n        }\n    }\n\n    /// Executes `CallFunctionKw` opcode.\n    ///\n    /// Pops the callable, positional args, and keyword args from the stack,\n    /// builds the appropriate `ArgValues`, and calls the function.\n    pub(super) fn exec_call_function_kw(\n        &mut self,\n        pos_count: usize,\n        kwname_ids: Vec<StringId>,\n    ) -> Result<CallResult, RunError> {\n        let kw_count = kwname_ids.len();\n\n        // Pop keyword values (TOS is last kwarg value)\n        let kw_values = self.pop_n(kw_count);\n\n        // Pop positional arguments\n        let pos_args = self.pop_n(pos_count);\n\n        // Pop the callable\n        let callable = self.pop();\n        let this = self;\n        defer_drop!(callable, this);\n\n        // Build kwargs as Vec<(StringId, Value)>\n        let kwargs_inline: Vec<(StringId, Value)> = kwname_ids.into_iter().zip(kw_values).collect();\n\n        // Build ArgValues with both positional and keyword args\n        let args = if pos_args.is_empty() && kwargs_inline.is_empty() {\n            ArgValues::Empty\n        } else if pos_args.is_empty() {\n            ArgValues::Kwargs(KwargsValues::Inline(kwargs_inline))\n        } else {\n            ArgValues::ArgsKargs {\n                args: pos_args,\n                kwargs: KwargsValues::Inline(kwargs_inline),\n            }\n        };\n\n        this.call_function(callable, args)\n    }\n\n    /// Executes `CallAttr` opcode.\n    ///\n    /// Pops the object and arguments from the stack, calls the attribute,\n    /// and returns a `CallResult` which may indicate an OS or external call.\n    pub(super) fn exec_call_attr(&mut self, name_id: StringId, arg_count: usize) -> Result<CallResult, RunError> {\n        let args = self.pop_n_args(arg_count);\n        let obj = self.pop();\n        self.call_attr(obj, name_id, args)\n    }\n\n    /// Executes `CallAttrKw` opcode.\n    ///\n    /// Pops the object, positional args, and keyword args from the stack,\n    /// builds the appropriate `ArgValues`, and calls the attribute.\n    /// Returns a `CallResult` which may indicate an OS or external call.\n    pub(super) fn exec_call_attr_kw(\n        &mut self,\n        name_id: StringId,\n        pos_count: usize,\n        kwname_ids: Vec<StringId>,\n    ) -> Result<CallResult, RunError> {\n        let kw_count = kwname_ids.len();\n\n        // Pop keyword values (TOS is last kwarg value)\n        let kw_values = self.pop_n(kw_count);\n\n        // Pop positional arguments\n        let pos_args = self.pop_n(pos_count);\n\n        // Pop the object\n        let obj = self.pop();\n\n        // Build kwargs as Vec<(StringId, Value)>\n        let kwargs_inline: Vec<(StringId, Value)> = kwname_ids.into_iter().zip(kw_values).collect();\n\n        // Build ArgValues with both positional and keyword args\n        let args = if pos_args.is_empty() && kwargs_inline.is_empty() {\n            ArgValues::Empty\n        } else if pos_args.is_empty() {\n            ArgValues::Kwargs(KwargsValues::Inline(kwargs_inline))\n        } else {\n            ArgValues::ArgsKargs {\n                args: pos_args,\n                kwargs: KwargsValues::Inline(kwargs_inline),\n            }\n        };\n\n        self.call_attr(obj, name_id, args)\n    }\n\n    /// Executes `CallFunctionExtended` opcode.\n    ///\n    /// Handles calls with `*args` and/or `**kwargs` unpacking.\n    pub(super) fn exec_call_function_extended(&mut self, has_kwargs: bool) -> Result<CallResult, RunError> {\n        // Pop kwargs dict if present\n        let kwargs = if has_kwargs { Some(self.pop()) } else { None };\n\n        // Pop args tuple\n        let args_tuple = self.pop();\n\n        // Pop callable\n        let callable = self.pop();\n\n        // Unpack and call\n        self.call_function_extended(callable, args_tuple, kwargs)\n    }\n\n    /// Executes `CallAttrExtended` opcode.\n    ///\n    /// Handles method calls with `*args` and/or `**kwargs` unpacking.\n    pub(super) fn exec_call_attr_extended(\n        &mut self,\n        name_id: StringId,\n        has_kwargs: bool,\n    ) -> Result<CallResult, RunError> {\n        // Pop kwargs dict if present\n        let kwargs = if has_kwargs { Some(self.pop()) } else { None };\n\n        // Pop args tuple\n        let args_tuple = self.pop();\n\n        // Pop the receiver object\n        let obj = self.pop();\n\n        // Unpack and call\n        self.call_attr_extended(obj, name_id, args_tuple, kwargs)\n    }\n\n    // ========================================================================\n    // Internal Call Helpers\n    // ========================================================================\n\n    /// Pops n arguments from the stack and wraps them in `ArgValues`.\n    fn pop_n_args(&mut self, n: usize) -> ArgValues {\n        match n {\n            0 => ArgValues::Empty,\n            1 => ArgValues::One(self.pop()),\n            2 => {\n                let b = self.pop();\n                let a = self.pop();\n                ArgValues::Two(a, b)\n            }\n            _ => ArgValues::ArgsKargs {\n                args: self.pop_n(n),\n                kwargs: KwargsValues::Empty,\n            },\n        }\n    }\n\n    /// Calls an attribute on an object.\n    ///\n    /// For heap-allocated objects (`Value::Ref`), dispatches to the type's\n    /// attribute call implementation via `Heap::call_attr()`, which may return\n    /// `CallResult::OsCall`, `CallResult::External`, or\n    /// `CallResult::MethodCall` for operations that require host involvement.\n    ///\n    /// For interned strings (`Value::InternString`), uses the unified `call_str_method`.\n    /// For interned bytes (`Value::InternBytes`), uses the unified `call_bytes_method`.\n    fn call_attr(&mut self, obj: Value, name_id: StringId, args: ArgValues) -> Result<CallResult, RunError> {\n        let this = self;\n        let attr = EitherStr::Interned(name_id);\n\n        match obj {\n            Value::Ref(heap_id) => {\n                defer_drop!(obj, this);\n                Heap::call_attr(this, heap_id, &attr, args)\n            }\n            Value::InternString(string_id) => {\n                // Call string method on interned string literal using the unified dispatcher\n                let s = this.interns.get_str(string_id);\n                call_str_method(s, name_id, args, this).map(CallResult::Value)\n            }\n            Value::InternBytes(bytes_id) => {\n                // Call bytes method on interned bytes literal using the unified dispatcher\n                let b = this.interns.get_bytes(bytes_id);\n                call_bytes_method(b, name_id, args, this).map(CallResult::Value)\n            }\n            Value::Builtin(Builtins::Type(t)) => {\n                // Handle classmethods on type objects like dict.fromkeys()\n                call_type_method(t, name_id, args, this).map(CallResult::Value)\n            }\n            _ => {\n                // Non-heap values without method support\n                let type_name = obj.py_type(this.heap);\n                args.drop_with_heap(this);\n                Err(ExcType::attribute_error(type_name, this.interns.get_str(name_id)))\n            }\n        }\n    }\n\n    /// Evaluates a function in a position that doesn't yet support suspending.\n    ///\n    /// Calls the function and, if it's a user-defined function that pushes a frame,\n    /// runs the VM until that frame returns.\n    ///\n    /// Returns an error for external/OS functions since those require the host to\n    /// execute them and resume, which this synchronous context cannot support.\n    pub(crate) fn evaluate_function(\n        &mut self,\n        ctx: &'static str,\n        callable: &Value,\n        args: ArgValues,\n    ) -> Result<Value, RunError> {\n        match self.call_function(callable, args)? {\n            CallResult::Value(v) => Ok(v),\n            CallResult::FramePushed => {\n                // A new frame was pushed for a defined function call - we need to run it\n                // to completion.\n                let stack_depth = self.frames.len();\n                // Mark the frame as an exit point from the `run()` loop\n                self.current_frame_mut().should_return = true;\n                match self.run()? {\n                    FrameExit::Return(v) => Ok(v),\n                    FrameExit::ResolveFutures(_)\n                    | FrameExit::ExternalCall { .. }\n                    | FrameExit::OsCall { .. }\n                    | FrameExit::MethodCall { .. }\n                    | FrameExit::NameLookup { .. } => {\n                        // Pop frames off the stack from this failed evaluation\n                        while self.frames.len() > stack_depth {\n                            self.pop_frame();\n                        }\n                        Err(RunError::internal(format!(\n                            \"{ctx}: external functions are not yet supported in this context\"\n                        )))\n                    }\n                }\n            }\n            CallResult::External(_, _)\n            | CallResult::OsCall(_, _)\n            | CallResult::MethodCall(_, _)\n            | CallResult::AwaitValue(_) => {\n                // External calls are not supported in this context since the caller doesn't support suspending\n                Err(RunError::internal(format!(\n                    \"{ctx}: external functions are not yet supported in this context\"\n                )))\n            }\n        }\n    }\n\n    /// Calls a callable value with the given arguments.\n    ///\n    /// Dispatches based on the callable type:\n    /// - `Value::Builtin`: calls builtin directly, returns `Push`\n    /// - `Value::ModuleFunction`: calls module function directly, returns `Push`\n    /// - `Value::ExtFunction`: returns `External` for caller to execute\n    /// - `Value::DefFunction`: pushes a new frame, returns `FramePushed`\n    /// - `Value::Ref`: checks for closure/function on heap\n    pub(crate) fn call_function(&mut self, callable: &Value, args: ArgValues) -> Result<CallResult, RunError> {\n        match callable {\n            Value::Builtin(builtin) => {\n                let result = builtin.call(self, args)?;\n                Ok(CallResult::Value(result))\n            }\n            Value::ModuleFunction(mf) => mf.call(self, args),\n            Value::ExtFunction(name_id) => {\n                // External function - return to caller to execute\n                Ok(CallResult::External(EitherStr::Interned(*name_id), args))\n            }\n            Value::DefFunction(func_id) => {\n                // Defined function without defaults or captured variables\n                self.call_def_function(*func_id, &[], &[], args)\n            }\n            Value::Ref(heap_id) => {\n                // Could be a closure or function with defaults - check heap\n                self.call_heap_callable(*heap_id, args)\n            }\n            _ => {\n                args.drop_with_heap(self);\n                let ty = callable.py_type(self.heap);\n                Err(ExcType::type_error(format!(\"'{ty}' object is not callable\")))\n            }\n        }\n    }\n\n    /// Handles calling a heap-allocated callable (closure, function with defaults, or external function).\n    fn call_heap_callable(&mut self, heap_id: HeapId, args: ArgValues) -> Result<CallResult, RunError> {\n        let (func_id, cells, defaults) = match self.heap.get(heap_id) {\n            HeapData::Closure(closure) => {\n                let cloned_cells = closure.cells.clone();\n                let cloned_defaults: Vec<Value> = closure.defaults.iter().map(|v| v.clone_with_heap(self)).collect();\n                (closure.func_id, cloned_cells, cloned_defaults)\n            }\n            HeapData::FunctionDefaults(fd) => {\n                let cloned_defaults: Vec<Value> = fd.defaults.iter().map(|v| v.clone_with_heap(self)).collect();\n                (fd.func_id, Vec::new(), cloned_defaults)\n            }\n            HeapData::ExtFunction(name) => {\n                // Heap-allocated external function with a non-interned name\n                let name = name.clone();\n                return Ok(CallResult::External(EitherStr::Heap(name), args));\n            }\n            _ => {\n                args.drop_with_heap(self);\n                return Err(ExcType::type_error(\"object is not callable\"));\n            }\n        };\n\n        let this = self;\n        defer_drop!(defaults, this);\n        this.call_def_function(func_id, &cells, defaults, args)\n    }\n\n    /// Calls a function with unpacked args tuple and optional kwargs dict.\n    ///\n    /// Used for `f(*args)` and `f(**kwargs)` style calls.\n    fn call_function_extended(\n        &mut self,\n        callable: Value,\n        args_tuple: Value,\n        kwargs: Option<Value>,\n    ) -> Result<CallResult, RunError> {\n        let this = self;\n        defer_drop!(args_tuple, this);\n        defer_drop!(callable, this);\n\n        // Extract positional args from tuple\n        let copied_args = this.extract_args_tuple(args_tuple);\n\n        // Build ArgValues from positional args and optional kwargs\n        let args = if let Some(kwargs_ref) = kwargs {\n            this.build_args_with_kwargs(copied_args, kwargs_ref)?\n        } else {\n            Self::build_args_positional_only(copied_args)\n        };\n\n        // Call the function (args_tuple guard drops at scope exit)\n        this.call_function(callable, args)\n    }\n\n    /// Calls a method with unpacked args tuple and optional kwargs dict.\n    ///\n    /// Used for `obj.method(*args)` and `obj.method(**kwargs)` style calls.\n    fn call_attr_extended(\n        &mut self,\n        obj: Value,\n        name_id: StringId,\n        args_tuple: Value,\n        kwargs: Option<Value>,\n    ) -> Result<CallResult, RunError> {\n        let this = self;\n        defer_drop!(args_tuple, this);\n\n        // Extract positional args from tuple\n        let copied_args = this.extract_args_tuple_for_attr(args_tuple);\n\n        // Build ArgValues from positional args and optional kwargs\n        let args = if let Some(kwargs_ref) = kwargs {\n            this.build_args_with_kwargs_for_attr(copied_args, kwargs_ref)?\n        } else {\n            Self::build_args_positional_only(copied_args)\n        };\n\n        // Call the method (args_tuple guard drops at scope exit)\n        this.call_attr(obj, name_id, args)\n    }\n\n    /// Extracts arguments from a tuple for `CallFunctionExtended`.\n    ///\n    /// # Panics\n    /// Panics if `args_tuple` is not a tuple. This indicates a compiler bug since\n    /// the compiler always emits `ListToTuple` before `CallFunctionExtended`.\n    fn extract_args_tuple(&mut self, args_tuple: &Value) -> Vec<Value> {\n        let Value::Ref(id) = args_tuple else {\n            unreachable!(\"CallFunctionExtended: args_tuple must be a Ref\")\n        };\n        let HeapData::Tuple(tuple) = self.heap.get(*id) else {\n            unreachable!(\"CallFunctionExtended: args_tuple must be a Tuple\")\n        };\n        tuple.as_slice().iter().map(|v| v.clone_with_heap(self)).collect()\n    }\n\n    /// Builds `ArgValues` with kwargs for `CallFunctionExtended`.\n    ///\n    /// # Panics\n    /// Panics if `kwargs_ref` is not a dict. This indicates a compiler bug since\n    /// the compiler always emits `BuildDict` before `CallFunctionExtended` with kwargs.\n    fn build_args_with_kwargs(&mut self, copied_args: Vec<Value>, kwargs_ref: Value) -> Result<ArgValues, RunError> {\n        let this = self;\n        defer_drop!(kwargs_ref, this);\n\n        // Extract kwargs dict items\n        let Value::Ref(id) = kwargs_ref else {\n            unreachable!(\"CallFunctionExtended: kwargs must be a Ref\")\n        };\n        let HeapData::Dict(dict) = this.heap.get(*id) else {\n            unreachable!(\"CallFunctionExtended: kwargs must be a Dict\")\n        };\n        let copied_kwargs: Vec<(Value, Value)> = dict\n            .iter()\n            .map(|(k, v)| (k.clone_with_heap(this), v.clone_with_heap(this)))\n            .collect();\n\n        let kwargs_values = if copied_kwargs.is_empty() {\n            KwargsValues::Empty\n        } else {\n            let kwargs_dict = Dict::from_pairs(copied_kwargs, this)?;\n            KwargsValues::Dict(kwargs_dict)\n        };\n\n        Ok(\n            if copied_args.is_empty() && matches!(kwargs_values, KwargsValues::Empty) {\n                ArgValues::Empty\n            } else if copied_args.is_empty() {\n                ArgValues::Kwargs(kwargs_values)\n            } else {\n                ArgValues::ArgsKargs {\n                    args: copied_args,\n                    kwargs: kwargs_values,\n                }\n            },\n        )\n    }\n\n    /// Builds `ArgValues` from positional args only.\n    fn build_args_positional_only(copied_args: Vec<Value>) -> ArgValues {\n        match copied_args.len() {\n            0 => ArgValues::Empty,\n            1 => ArgValues::One(copied_args.into_iter().next().unwrap()),\n            2 => {\n                let mut iter = copied_args.into_iter();\n                ArgValues::Two(iter.next().unwrap(), iter.next().unwrap())\n            }\n            _ => ArgValues::ArgsKargs {\n                args: copied_args,\n                kwargs: KwargsValues::Empty,\n            },\n        }\n    }\n\n    /// Extracts arguments from a tuple for `CallAttrExtended`.\n    ///\n    /// # Panics\n    /// Panics if `args_tuple` is not a tuple. This indicates a compiler bug since\n    /// the compiler always emits `ListToTuple` before `CallAttrExtended`.\n    fn extract_args_tuple_for_attr(&mut self, args_tuple: &Value) -> Vec<Value> {\n        let Value::Ref(id) = args_tuple else {\n            unreachable!(\"CallAttrExtended: args_tuple must be a Ref\")\n        };\n        let HeapData::Tuple(tuple) = self.heap.get(*id) else {\n            unreachable!(\"CallAttrExtended: args_tuple must be a Tuple\")\n        };\n        tuple.as_slice().iter().map(|v| v.clone_with_heap(self)).collect()\n    }\n\n    /// Builds `ArgValues` with kwargs for `CallAttrExtended`.\n    ///\n    /// # Panics\n    /// Panics if `kwargs_ref` is not a dict. This indicates a compiler bug since\n    /// the compiler always emits `BuildDict` before `CallAttrExtended` with kwargs.\n    fn build_args_with_kwargs_for_attr(\n        &mut self,\n        copied_args: Vec<Value>,\n        kwargs_ref: Value,\n    ) -> Result<ArgValues, RunError> {\n        let this = self;\n        defer_drop!(kwargs_ref, this);\n\n        // Extract kwargs dict items\n        let Value::Ref(id) = kwargs_ref else {\n            unreachable!(\"CallAttrExtended: kwargs must be a Ref\")\n        };\n        let HeapData::Dict(dict) = this.heap.get(*id) else {\n            unreachable!(\"CallAttrExtended: kwargs must be a Dict\")\n        };\n        let copied_kwargs: Vec<(Value, Value)> = dict\n            .iter()\n            .map(|(k, v)| (k.clone_with_heap(this.heap), v.clone_with_heap(this.heap)))\n            .collect();\n\n        let kwargs_values = if copied_kwargs.is_empty() {\n            KwargsValues::Empty\n        } else {\n            let kwargs_dict = Dict::from_pairs(copied_kwargs, this)?;\n            KwargsValues::Dict(kwargs_dict)\n        };\n\n        Ok(\n            if copied_args.is_empty() && matches!(kwargs_values, KwargsValues::Empty) {\n                ArgValues::Empty\n            } else if copied_args.is_empty() {\n                ArgValues::Kwargs(kwargs_values)\n            } else {\n                ArgValues::ArgsKargs {\n                    args: copied_args,\n                    kwargs: kwargs_values,\n                }\n            },\n        )\n    }\n\n    // ========================================================================\n    // Frame Setup\n    // ========================================================================\n\n    /// Calls a defined function by pushing a new frame or creating a coroutine.\n    ///\n    /// For sync functions: sets up the function's namespace with bound arguments,\n    /// cell variables, and free variables, then pushes a new frame.\n    ///\n    /// For async functions: binds arguments immediately but returns a Coroutine\n    /// instead of pushing a frame. The coroutine stores the pre-bound namespace\n    /// and will be executed when awaited.\n    fn call_def_function(\n        &mut self,\n        func_id: FunctionId,\n        cells: &[HeapId],\n        defaults: &[Value],\n        args: ArgValues,\n    ) -> Result<CallResult, RunError> {\n        let func = self.interns.get_function(func_id);\n\n        if func.is_async {\n            self.create_coroutine(func_id, cells, defaults, args)\n        } else {\n            self.call_sync_function(func_id, cells, defaults, args)\n        }\n    }\n\n    /// Creates a Coroutine for an async function call.\n    ///\n    /// The coroutine is executed when awaited via Await.\n    fn create_coroutine(\n        &mut self,\n        func_id: FunctionId,\n        cells: &[HeapId],\n        defaults: &[Value],\n        args: ArgValues,\n    ) -> Result<CallResult, RunError> {\n        let func = self.interns.get_function(func_id);\n\n        // 1. Create namespace for the coroutine with bound arguments and captured cells.\n        let namespace = Vec::with_capacity(func.namespace_size);\n        let mut namespace_guard = HeapGuard::new(namespace, self);\n        let (namespace, this) = namespace_guard.as_parts_mut();\n\n        // 2. Bind arguments to parameters\n        func.signature.bind(args, defaults, this, func.name, namespace)?;\n\n        // 3. Create cells for variables captured by nested functions\n        {\n            let param_count = func.signature.total_slots();\n            for (i, maybe_param_idx) in func.cell_param_indices.iter().enumerate() {\n                let cell_slot = param_count + i;\n                let cell_value = if let Some(param_idx) = maybe_param_idx {\n                    namespace[*param_idx].clone_with_heap(this.heap)\n                } else {\n                    Value::Undefined\n                };\n                let cell_id = this.heap.allocate(HeapData::Cell(CellValue(cell_value)))?;\n                namespace.resize_with(cell_slot, || Value::Undefined);\n                namespace.push(Value::Ref(cell_id));\n            }\n\n            // 4. Copy captured cells (free vars) into namespace\n            let free_var_start = param_count + func.cell_var_count;\n            for (i, &cell_id) in cells.iter().enumerate() {\n                this.heap.inc_ref(cell_id);\n                let slot = free_var_start + i;\n                namespace.resize_with(slot, || Value::Undefined);\n                namespace.push(Value::Ref(cell_id));\n            }\n\n            // 5. Fill remaining slots with Undefined\n            namespace.resize_with(func.namespace_size, || Value::Undefined);\n        }\n\n        // 6. Create Coroutine on heap\n        let (namespace, this) = namespace_guard.into_parts();\n        let coroutine = Coroutine::new(func_id, namespace);\n        let coroutine_id = this.heap.allocate(HeapData::Coroutine(coroutine))?;\n\n        Ok(CallResult::Value(Value::Ref(coroutine_id)))\n    }\n\n    /// Calls a sync function by pushing a new frame.\n    ///\n    /// Sets up the function's namespace with bound arguments, cell variables,\n    /// and free variables (captured from enclosing scope for closures).\n    ///\n    /// Locals are built directly on the VM stack using a [`StackGuard`] that\n    /// automatically rolls back on error. The frame's `stack_base` points to\n    /// the start of this locals region, and operands are pushed above it.\n    fn call_sync_function(\n        &mut self,\n        func_id: FunctionId,\n        cells: &[HeapId],\n        defaults: &[Value],\n        args: ArgValues,\n    ) -> Result<CallResult, RunError> {\n        let call_position = self.current_position();\n        let stack_base = self.stack.len();\n\n        let func = self.interns.get_function(func_id);\n        let namespace_size = func.namespace_size;\n        let locals_count = u16::try_from(namespace_size).expect(\"function namespace size exceeds u16\");\n\n        // Track memory for this frame's locals\n        let size = namespace_size * std::mem::size_of::<Value>();\n        self.heap.tracker_mut().on_allocate(|| size)?;\n\n        // 1. Create namespace for the frame in a temporary vec, will extend to stack later\n        let namespace = Vec::with_capacity(func.namespace_size);\n        let mut namespace_guard = HeapGuard::new(namespace, self);\n        let (namespace, this) = namespace_guard.as_parts_mut();\n\n        // 2. Bind arguments to parameters\n        {\n            let bind_result = func.signature.bind(args, defaults, this, func.name, namespace);\n\n            if let Err(e) = bind_result {\n                this.heap.tracker_mut().on_free(|| size);\n                return Err(e);\n            }\n        }\n\n        // 3. Create cells for variables captured by nested functions\n        {\n            let param_count = func.signature.total_slots();\n            for (i, maybe_param_idx) in func.cell_param_indices.iter().enumerate() {\n                let cell_slot = param_count + i;\n                let cell_value = if let Some(param_idx) = maybe_param_idx {\n                    namespace[*param_idx].clone_with_heap(this.heap)\n                } else {\n                    Value::Undefined\n                };\n                let cell_id = this.heap.allocate(HeapData::Cell(CellValue(cell_value)))?;\n                namespace.resize_with(cell_slot, || Value::Undefined);\n                namespace.push(Value::Ref(cell_id));\n            }\n\n            // 4. Copy captured cells (free vars) into namespace\n            let free_var_start = param_count + func.cell_var_count;\n            for (i, &cell_id) in cells.iter().enumerate() {\n                this.heap.inc_ref(cell_id);\n                let slot = free_var_start + i;\n                namespace.resize_with(slot, || Value::Undefined);\n                namespace.push(Value::Ref(cell_id));\n            }\n\n            // 5. Fill remaining slots with Undefined\n            namespace.resize_with(namespace_size, || Value::Undefined);\n        }\n\n        let code = &func.code;\n\n        // 6. Commit the guard (no rollback) and push the frame\n        let (namespace, this) = namespace_guard.into_parts();\n        this.stack.extend(namespace);\n\n        this.push_frame(CallFrame::new_function(\n            code,\n            stack_base,\n            locals_count,\n            func_id,\n            Some(call_position),\n        ))?;\n\n        Ok(CallResult::FramePushed)\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/collections.rs",
    "content": "//! Collection building and unpacking helpers for the VM.\n\nuse smallvec::SmallVec;\n\nuse super::VM;\nuse crate::{\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunError, SimpleException},\n    heap::{Heap, HeapData, HeapGuard},\n    heap_data::HeapDataMut,\n    intern::StringId,\n    resource::ResourceTracker,\n    types::{Dict, List, PyTrait, Set, Slice, Type, allocate_tuple, slice::value_to_option_i64, str::allocate_char},\n    value::Value,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Builds a list from the top n stack values.\n    pub(super) fn build_list(&mut self, count: usize) -> Result<(), RunError> {\n        let items = self.pop_n(count);\n        let list = List::new(items);\n        let heap_id = self.heap.allocate(HeapData::List(list))?;\n        self.push(Value::Ref(heap_id));\n        Ok(())\n    }\n\n    /// Builds a tuple from the top n stack values.\n    ///\n    /// Uses the empty tuple singleton when count is 0, and SmallVec\n    /// optimization for small tuples (≤2 elements).\n    pub(super) fn build_tuple(&mut self, count: usize) -> Result<(), RunError> {\n        let items = self.pop_n(count);\n        let value = allocate_tuple(items.into(), self.heap)?;\n        self.push(value);\n        Ok(())\n    }\n\n    /// Builds a dict from the top 2n stack values (key/value pairs).\n    pub(super) fn build_dict(&mut self, count: usize) -> Result<(), RunError> {\n        let items = self.pop_n(count * 2);\n        let mut dict = Dict::new();\n        // Use into_iter to consume items by value, avoiding clone and proper ownership transfer\n        let mut iter = items.into_iter();\n        while let (Some(key), Some(value)) = (iter.next(), iter.next()) {\n            dict.set(key, value, self)?;\n        }\n        let heap_id = self.heap.allocate(HeapData::Dict(dict))?;\n        self.push(Value::Ref(heap_id));\n        Ok(())\n    }\n\n    /// Builds a set from the top n stack values.\n    pub(super) fn build_set(&mut self, count: usize) -> Result<(), RunError> {\n        let items = self.pop_n(count);\n        let mut set = Set::new();\n        for item in items {\n            set.add(item, self)?;\n        }\n        let heap_id = self.heap.allocate(HeapData::Set(set))?;\n        self.push(Value::Ref(heap_id));\n        Ok(())\n    }\n\n    /// Builds a slice object from the top 3 stack values.\n    ///\n    /// Stack: [start, stop, step] -> [slice]\n    /// Each value can be None (for default) or an integer.\n    pub(super) fn build_slice(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let step_val = this.pop();\n        defer_drop!(step_val, this);\n        let stop_val = this.pop();\n        defer_drop!(stop_val, this);\n        let start_val = this.pop();\n        defer_drop!(start_val, this);\n\n        let start = value_to_option_i64(start_val)?;\n        let stop = value_to_option_i64(stop_val)?;\n        let step = value_to_option_i64(step_val)?;\n\n        let slice = Slice::new(start, stop, step);\n        let heap_id = this.heap.allocate(HeapData::Slice(slice))?;\n        this.push(Value::Ref(heap_id));\n        Ok(())\n    }\n\n    /// Extends a list with items from an iterable, for PEP 448 `*expr` literal unpacking.\n    ///\n    /// Stack: [list, iterable] -> [list]\n    /// Pops the iterable, extends the list in place, leaves list on stack.\n    ///\n    /// Raises `TypeError(\"Value after * must be an iterable, not {type}\")` for non-iterables,\n    /// matching CPython's message for list/tuple literal unpacking (`[*x]`, `(*x,)`).\n    ///\n    /// Uses `HeapGuard` for `list_ref` because it is pushed back on success,\n    /// and `defer_drop!` for `iterable` because it is always dropped.\n    pub(super) fn list_extend(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let iterable = this.pop();\n        defer_drop!(iterable, this);\n        // HeapGuard for list_ref: pushed back on success via into_parts, dropped on error\n        let mut list_ref_guard = HeapGuard::new(this.pop(), this);\n        let (list_ref, this) = list_ref_guard.as_parts();\n\n        let copied_items: Vec<Value> = match iterable {\n            Value::Ref(id) => match this.heap.get(*id) {\n                HeapData::List(list) => list.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect(),\n                HeapData::Tuple(tuple) => tuple.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect(),\n                HeapData::Set(set) => set.storage().iter().map(|v| v.clone_with_heap(this.heap)).collect(),\n                HeapData::Dict(dict) => dict.iter().map(|(k, _)| k.clone_with_heap(this.heap)).collect(),\n                HeapData::Str(s) => {\n                    // Need to allocate strings for each character\n                    let chars: Vec<char> = s.as_str().chars().collect();\n                    let mut items = Vec::with_capacity(chars.len());\n                    for c in chars {\n                        items.push(allocate_char(c, this.heap)?);\n                    }\n                    items\n                }\n                _ => {\n                    let type_ = iterable.py_type(this.heap);\n                    return Err(ExcType::type_error_value_after_star(type_));\n                }\n            },\n            Value::InternString(id) => {\n                let s = this.interns.get_str(*id);\n                let chars: Vec<char> = s.chars().collect();\n                let mut items = Vec::with_capacity(chars.len());\n                for c in chars {\n                    items.push(allocate_char(c, this.heap)?);\n                }\n                items\n            }\n            _ => {\n                let type_ = iterable.py_type(this.heap);\n                return Err(ExcType::type_error_value_after_star(type_));\n            }\n        };\n\n        // Check if any copied items are refs (for updating contains_refs)\n        let has_refs = copied_items.iter().any(|v| matches!(v, Value::Ref(_)));\n\n        // Extend the list\n        if let Value::Ref(id) = list_ref\n            && let HeapDataMut::List(list) = this.heap.get_mut(*id)\n        {\n            // Update contains_refs before extending\n            if has_refs {\n                list.set_contains_refs();\n            }\n            list.as_vec_mut().extend(copied_items);\n        }\n\n        // Mark potential cycle after the mutable borrow ends\n        if has_refs {\n            this.heap.mark_potential_cycle();\n        }\n\n        // Push list_ref back on the stack (don't drop it)\n        let (list_ref, this) = list_ref_guard.into_parts();\n        this.push(list_ref);\n        Ok(())\n    }\n\n    /// Converts a list to a tuple.\n    ///\n    /// Stack: [list] -> [tuple]\n    pub(super) fn list_to_tuple(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let list_ref = this.pop();\n        defer_drop!(list_ref, this);\n\n        let copied_items: SmallVec<_> = if let Value::Ref(id) = list_ref {\n            if let HeapData::List(list) = this.heap.get(*id) {\n                list.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect()\n            } else {\n                return Err(RunError::internal(\"ListToTuple: expected list\"));\n            }\n        } else {\n            return Err(RunError::internal(\"ListToTuple: expected list ref\"));\n        };\n\n        // list_ref is dropped by the guard at scope exit; allocate the tuple\n        let value = allocate_tuple(copied_items, this.heap)?;\n        this.push(value);\n        Ok(())\n    }\n\n    /// Merges a mapping into a dict for **kwargs unpacking.\n    ///\n    /// Stack: [dict, mapping] -> [dict]\n    /// Validates that mapping is a dict and that keys are strings.\n    ///\n    /// Uses `defer_drop!` for `mapping` (always dropped) and `HeapGuard` for\n    /// `dict_ref` (pushed back on success, dropped on error).\n    pub(super) fn dict_merge(&mut self, func_name_id: u16) -> Result<(), RunError> {\n        let this = self;\n\n        let mapping = this.pop();\n        defer_drop!(mapping, this);\n        // HeapGuard for dict_ref: pushed back on success via into_parts, dropped on error\n        let mut dict_ref_guard = HeapGuard::new(this.pop(), this);\n        let (dict_ref, this) = dict_ref_guard.as_parts();\n\n        // Get function name for error messages\n        let func_name = if func_name_id == 0xFFFF {\n            \"<unknown>\".to_string()\n        } else {\n            this.interns.get_str(StringId::from_index(func_name_id)).to_string()\n        };\n\n        // Check that mapping is a dict (Ref pointing to Dict) and clone key-value pairs\n        let copied_items: Vec<(Value, Value)> = if let Value::Ref(id) = mapping {\n            if let HeapData::Dict(dict) = this.heap.get(*id) {\n                dict.iter()\n                    .map(|(k, v)| (k.clone_with_heap(this.heap), v.clone_with_heap(this.heap)))\n                    .collect()\n            } else {\n                let type_name = mapping.py_type(this.heap).to_string();\n                return Err(ExcType::type_error_kwargs_not_mapping(&func_name, &type_name));\n            }\n        } else {\n            let type_name = mapping.py_type(this.heap).to_string();\n            return Err(ExcType::type_error_kwargs_not_mapping(&func_name, &type_name));\n        };\n\n        // Merge into the dict, validating string keys\n        let dict_id = if let Value::Ref(id) = dict_ref {\n            *id\n        } else {\n            return Err(RunError::internal(\"DictMerge: expected dict ref\"));\n        };\n\n        for (key, value) in copied_items {\n            // Validate key is a string (InternString or heap-allocated Str)\n            let is_string = match &key {\n                Value::InternString(_) => true,\n                Value::Ref(id) => matches!(this.heap.get(*id), HeapData::Str(_)),\n                _ => false,\n            };\n            if !is_string {\n                key.drop_with_heap(this);\n                value.drop_with_heap(this);\n                return Err(ExcType::type_error_kwargs_nonstring_key());\n            }\n\n            // Get the string key for error messages (needed before moving key into closure)\n            let key_str = match &key {\n                Value::InternString(id) => this.interns.get_str(*id).to_string(),\n                Value::Ref(id) => {\n                    if let HeapData::Str(s) = this.heap.get(*id) {\n                        s.as_str().to_string()\n                    } else {\n                        \"<unknown>\".to_string()\n                    }\n                }\n                _ => \"<unknown>\".to_string(),\n            };\n\n            // Use with_entry_mut to avoid borrow conflict: takes data out temporarily\n            let result = Heap::with_entry_mut(this, dict_id, |this, data| {\n                if let HeapDataMut::Dict(dict) = data {\n                    dict.set(key, value, this)\n                } else {\n                    Err(RunError::internal(\"DictMerge: entry is not a Dict\"))\n                }\n            });\n\n            // If set returned Some, the key already existed (duplicate kwarg)\n            if let Some(old_value) = result? {\n                old_value.drop_with_heap(this);\n                return Err(ExcType::type_error_multiple_values(&func_name, &key_str));\n            }\n        }\n\n        // Push dict_ref back on the stack (don't drop it)\n        let (dict_ref, this) = dict_ref_guard.into_parts();\n        this.push(dict_ref);\n        Ok(())\n    }\n\n    // ========================================================================\n    // PEP 448 Literal Building\n    // ========================================================================\n\n    /// Silently merges a mapping into the dict literal at `depth` on the stack.\n    ///\n    /// Used for `{**x, ...}` dict literals where later keys silently overwrite\n    /// earlier ones (unlike [`dict_merge`] which raises `TypeError` on duplicate keys\n    /// and is used for function-call `**kwargs`).\n    ///\n    /// Stack (depth = 0): `[..., dict, mapping]` → `[..., dict]`\n    ///\n    /// # Errors\n    ///\n    /// Returns `TypeError: '{type}' object is not a mapping` if the TOS is not a dict.\n    pub(super) fn dict_update(&mut self, depth: usize) -> Result<(), RunError> {\n        let this = self;\n\n        let mapping = this.pop();\n        defer_drop!(mapping, this);\n\n        // Clone all key/value pairs out of the mapping before mutating the target dict\n        let copied_items: Vec<(Value, Value)> = if let Value::Ref(id) = mapping {\n            if let HeapData::Dict(dict) = this.heap.get(*id) {\n                dict.iter()\n                    .map(|(k, v)| (k.clone_with_heap(this.heap), v.clone_with_heap(this.heap)))\n                    .collect()\n            } else {\n                let type_ = mapping.py_type(this.heap);\n                return Err(ExcType::type_error_not_mapping(type_));\n            }\n        } else {\n            let type_ = mapping.py_type(this.heap);\n            return Err(ExcType::type_error_not_mapping(type_));\n        };\n\n        // The target dict sits at `depth` positions below TOS (which is now gone after pop)\n        let stack_len = this.stack.len();\n        let dict_pos = stack_len - 1 - depth;\n        // SAFETY: the compiler always emits BuildDict before DictUpdate, so the\n        // target is always a Value::Ref.  This is a VM invariant: reaching this else\n        // arm means a compiler bug.\n        let Value::Ref(dict_id) = this.stack[dict_pos] else {\n            unreachable!(\"DictUpdate: target is always a Ref — compiler invariant\")\n        };\n\n        for (key, value) in copied_items {\n            let old = Heap::with_entry_mut(this, dict_id, |this, data| {\n                if let HeapDataMut::Dict(dict) = data {\n                    dict.set(key, value, this)\n                } else {\n                    // SAFETY: dict_id was obtained from a Value::Ref on the stack that\n                    // was created by BuildDict; it always refers to a HeapData::Dict.\n                    unreachable!(\"DictUpdate: heap entry is always a Dict — compiler invariant\")\n                }\n            })?;\n            // Silently drop any old value — PEP 448 dict literals allow duplicate keys\n            if let Some(old_val) = old {\n                old_val.drop_with_heap(this.heap);\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Extends a set literal with all items from an iterable.\n    ///\n    /// Used for `{*x, ...}` set literals (PEP 448). Follows the same item-copying\n    /// pattern as [`list_extend`]; raises `TypeError` for non-iterable sources.\n    ///\n    /// Stack (depth = 0): `[..., set, iterable]` → `[..., set]`\n    ///\n    /// # Errors\n    ///\n    /// Returns `TypeError: '{type}' object is not iterable` if TOS is not iterable.\n    pub(super) fn set_extend(&mut self, depth: usize) -> Result<(), RunError> {\n        let this = self;\n\n        let iterable = this.pop();\n        defer_drop!(iterable, this);\n\n        // Clone items from the iterable (same sources as list_extend)\n        let copied_items: Vec<Value> = match iterable {\n            Value::Ref(id) => match this.heap.get(*id) {\n                HeapData::List(list) => list.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect(),\n                HeapData::Tuple(tuple) => tuple.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect(),\n                HeapData::Set(set) => set.storage().iter().map(|v| v.clone_with_heap(this.heap)).collect(),\n                HeapData::Dict(dict) => dict.iter().map(|(k, _)| k.clone_with_heap(this.heap)).collect(),\n                HeapData::Str(s) => {\n                    let chars: Vec<char> = s.as_str().chars().collect();\n                    let mut items = Vec::with_capacity(chars.len());\n                    for c in chars {\n                        items.push(allocate_char(c, this.heap)?);\n                    }\n                    items\n                }\n                _ => {\n                    let type_ = iterable.py_type(this.heap);\n                    return Err(ExcType::type_error_not_iterable(type_));\n                }\n            },\n            Value::InternString(id) => {\n                let s = this.interns.get_str(*id);\n                let chars: Vec<char> = s.chars().collect();\n                let mut items = Vec::with_capacity(chars.len());\n                for c in chars {\n                    items.push(allocate_char(c, this.heap)?);\n                }\n                items\n            }\n            _ => {\n                let type_ = iterable.py_type(this.heap);\n                return Err(ExcType::type_error_not_iterable(type_));\n            }\n        };\n\n        // The target set sits at `depth` positions below TOS (which is now gone after pop)\n        let stack_len = this.stack.len();\n        let set_pos = stack_len - 1 - depth;\n        // SAFETY: the compiler always emits BuildSet before SetExtend, so the\n        // target is always a Value::Ref.  This is a VM invariant: reaching this else\n        // arm means a compiler bug.\n        let Value::Ref(set_id) = this.stack[set_pos] else {\n            unreachable!(\"SetExtend: target is always a Ref — compiler invariant\")\n        };\n\n        for item in copied_items {\n            Heap::with_entry_mut(this, set_id, |this, data| {\n                if let HeapDataMut::Set(set) = data {\n                    set.add(item, this)\n                } else {\n                    // SAFETY: set_id was obtained from a Value::Ref on the stack that\n                    // was created by BuildSet; it always refers to a HeapData::Set.\n                    unreachable!(\"SetExtend: heap entry is always a Set — compiler invariant\")\n                }\n            })?;\n        }\n\n        Ok(())\n    }\n\n    // ========================================================================\n    // Comprehension Building\n    // ========================================================================\n\n    /// Appends TOS to list for comprehension.\n    ///\n    /// Stack: [..., list, iter1, ..., iterN, value] -> [..., list, iter1, ..., iterN]\n    /// The `depth` parameter is the number of iterators between the list and the value.\n    /// List is at stack position: len - 2 - depth (0-indexed from bottom).\n    pub(super) fn list_append(&mut self, depth: usize) -> Result<(), RunError> {\n        let value = self.pop();\n        let stack_len = self.stack.len();\n        let list_pos = stack_len - 1 - depth;\n\n        // Get the list reference\n        let Value::Ref(list_id) = self.stack[list_pos] else {\n            value.drop_with_heap(self);\n            return Err(RunError::internal(\"ListAppend: expected list ref on stack\"));\n        };\n\n        // Append to the list using with_entry_mut to handle proper contains_refs tracking\n        Heap::with_entry_mut(self, list_id, |this, data| {\n            if let HeapDataMut::List(list) = data {\n                list.append(this.heap, value);\n                Ok(())\n            } else {\n                value.drop_with_heap(this);\n                Err(RunError::internal(\"ListAppend: expected list on heap\"))\n            }\n        })\n    }\n\n    /// Adds TOS to set for comprehension.\n    ///\n    /// Stack: [..., set, iter1, ..., iterN, value] -> [..., set, iter1, ..., iterN]\n    /// The `depth` parameter is the number of iterators between the set and the value.\n    /// May raise TypeError if value is unhashable.\n    pub(super) fn set_add(&mut self, depth: usize) -> Result<(), RunError> {\n        let value = self.pop();\n        let stack_len = self.stack.len();\n        let set_pos = stack_len - 1 - depth;\n\n        // Get the set reference\n        let Value::Ref(set_id) = self.stack[set_pos] else {\n            value.drop_with_heap(self);\n            return Err(RunError::internal(\"SetAdd: expected set ref on stack\"));\n        };\n\n        // Add to the set using with_entry_mut to avoid borrow conflicts\n        Heap::with_entry_mut(self, set_id, |this, data| {\n            if let HeapDataMut::Set(set) = data {\n                set.add(value, this)\n            } else {\n                value.drop_with_heap(this);\n                Err(RunError::internal(\"SetAdd: expected set on heap\"))\n            }\n        })?;\n\n        Ok(())\n    }\n\n    /// Sets dict[key] = value for comprehension.\n    ///\n    /// Stack: [..., dict, iter1, ..., iterN, key, value] -> [..., dict, iter1, ..., iterN]\n    /// The `depth` parameter is the number of iterators between the dict and the key-value pair.\n    /// May raise TypeError if key is unhashable.\n    pub(super) fn dict_set_item(&mut self, depth: usize) -> Result<(), RunError> {\n        let value = self.pop();\n        let key = self.pop();\n        let stack_len = self.stack.len();\n        let dict_pos = stack_len - 1 - depth;\n\n        // Get the dict reference\n        let Value::Ref(dict_id) = self.stack[dict_pos] else {\n            key.drop_with_heap(self);\n            value.drop_with_heap(self);\n            return Err(RunError::internal(\"DictSetItem: expected dict ref on stack\"));\n        };\n\n        // Set item in the dict using with_entry_mut to avoid borrow conflicts\n        let old_value = Heap::with_entry_mut(self, dict_id, |this, data| {\n            if let HeapDataMut::Dict(dict) = data {\n                dict.set(key, value, this)\n            } else {\n                key.drop_with_heap(this);\n                value.drop_with_heap(this);\n                Err(RunError::internal(\"DictSetItem: expected dict on heap\"))\n            }\n        })?;\n\n        // Drop old value if key already existed\n        if let Some(old) = old_value {\n            old.drop_with_heap(self);\n        }\n\n        Ok(())\n    }\n\n    // ========================================================================\n    // Unpacking\n    // ========================================================================\n\n    /// Unpacks a sequence into n values on the stack.\n    ///\n    /// Supports lists, tuples, and strings. For strings, each character becomes\n    /// a separate single-character string.\n    pub(super) fn unpack_sequence(&mut self, count: usize) -> Result<(), RunError> {\n        let this = self;\n\n        let value = this.pop();\n        defer_drop!(value, this);\n\n        // Copy values without incrementing refcounts (avoids borrow conflict with heap.get).\n        // For strings, we allocate new string values for each character.\n        let items: Vec<Value> = match value {\n            // Interned strings (string literals stored inline, not on heap)\n            Value::InternString(string_id) => {\n                let s = this.interns.get_str(*string_id);\n                let str_len = s.chars().count();\n                if str_len != count {\n                    return Err(unpack_size_error(count, str_len));\n                }\n                // Allocate each character as a new string\n                let mut items = Vec::with_capacity(str_len);\n                for c in s.chars() {\n                    items.push(allocate_char(c, this.heap)?);\n                }\n                // Push items in reverse order so first item is on top\n                for item in items.into_iter().rev() {\n                    this.push(item);\n                }\n                return Ok(());\n            }\n            // Heap-allocated sequences\n            Value::Ref(heap_id) => {\n                match this.heap.get(*heap_id) {\n                    HeapData::List(list) => {\n                        let list_len = list.len();\n                        if list_len != count {\n                            return Err(unpack_size_error(count, list_len));\n                        }\n                        list.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect()\n                    }\n                    HeapData::Tuple(tuple) => {\n                        let tuple_len = tuple.as_slice().len();\n                        if tuple_len != count {\n                            return Err(unpack_size_error(count, tuple_len));\n                        }\n                        tuple.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect()\n                    }\n                    HeapData::Str(s) => {\n                        let str_len = s.as_str().chars().count();\n                        if str_len != count {\n                            return Err(unpack_size_error(count, str_len));\n                        }\n                        let chars: Vec<char> = s.as_str().chars().collect();\n                        let mut items = Vec::with_capacity(chars.len());\n                        for c in chars {\n                            items.push(allocate_char(c, this.heap)?);\n                        }\n                        // Push items in reverse order so first item is on top\n                        for item in items.into_iter().rev() {\n                            this.push(item);\n                        }\n                        return Ok(());\n                    }\n                    other => {\n                        let type_name = other.py_type(this.heap);\n                        return Err(unpack_type_error(type_name));\n                    }\n                }\n            }\n            // Non-iterable types\n            _ => {\n                let type_name = value.py_type(this.heap);\n                return Err(unpack_type_error(type_name));\n            }\n        };\n\n        // Push items in reverse order so first item is on top\n        for item in items.into_iter().rev() {\n            this.push(item);\n        }\n        Ok(())\n    }\n\n    /// Unpacks a sequence with a starred target.\n    ///\n    /// `before` is the number of targets before the star, `after` is the number after.\n    /// The starred target collects all middle items into a list.\n    ///\n    /// For example, `first, *rest, last = [1, 2, 3, 4, 5]` has before=1, after=1.\n    /// After execution, the stack has: first (top), rest_list, last.\n    pub(super) fn unpack_ex(&mut self, before: usize, after: usize) -> Result<(), RunError> {\n        let this = self;\n\n        let value = this.pop();\n        defer_drop_mut!(value, this);\n\n        let min_items = before + after;\n\n        // Extract items from the sequence\n        let items: Vec<Value> = match value {\n            Value::InternString(string_id) => {\n                let s = this.interns.get_str(*string_id);\n                // Collect chars once to avoid double iteration over UTF-8 data\n                let chars: Vec<char> = s.chars().collect();\n                if chars.len() < min_items {\n                    return Err(unpack_ex_too_few_error(min_items, chars.len()));\n                }\n                // Allocate each character as a new string\n                let mut items = Vec::with_capacity(chars.len());\n                for c in chars {\n                    items.push(allocate_char(c, this.heap)?);\n                }\n                items\n            }\n            Value::Ref(heap_id) => {\n                match this.heap.get(*heap_id) {\n                    HeapData::List(list) => {\n                        let list_len = list.len();\n                        if list_len < min_items {\n                            return Err(unpack_ex_too_few_error(min_items, list_len));\n                        }\n                        list.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect()\n                    }\n                    HeapData::Tuple(tuple) => {\n                        let tuple_len = tuple.as_slice().len();\n                        if tuple_len < min_items {\n                            return Err(unpack_ex_too_few_error(min_items, tuple_len));\n                        }\n                        tuple.as_slice().iter().map(|v| v.clone_with_heap(this.heap)).collect()\n                    }\n                    HeapData::Str(s) => {\n                        // Collect chars once to avoid double iteration over UTF-8 data\n                        let chars: Vec<char> = s.as_str().chars().collect();\n                        if chars.len() < min_items {\n                            return Err(unpack_ex_too_few_error(min_items, chars.len()));\n                        }\n                        let mut items = Vec::with_capacity(chars.len());\n                        for c in chars {\n                            items.push(allocate_char(c, this.heap)?);\n                        }\n                        items\n                    }\n                    other => {\n                        let type_name = other.py_type(this.heap);\n                        return Err(unpack_type_error(type_name));\n                    }\n                }\n            }\n            _ => {\n                let type_name = value.py_type(this.heap);\n                return Err(unpack_type_error(type_name));\n            }\n        };\n\n        this.push_unpack_ex_results(items, before, after)\n    }\n\n    /// Helper to push unpacked items with starred target onto the stack.\n    ///\n    /// Takes a slice of items and creates the middle list.\n    fn push_unpack_ex_results(&mut self, items: Vec<Value>, before: usize, after: usize) -> Result<(), RunError> {\n        let this = self;\n\n        defer_drop_mut!(items, this);\n\n        // Items get pushed onto the stack backwards, so a lot of .rev() calls\n\n        for item in items.drain(items.len() - after..).rev() {\n            this.push(item);\n        }\n\n        // Middle items as a list (starred target)\n        let middle_list: Vec<Value> = items.drain(before..).collect();\n        let list_id = this.heap.allocate(HeapData::List(List::new(middle_list)))?;\n        this.push(Value::Ref(list_id));\n\n        // Before items\n        for item in items.drain(..).rev() {\n            this.push(item);\n        }\n\n        Ok(())\n    }\n}\n\n/// Creates the ValueError for star unpacking when there are too few values.\nfn unpack_ex_too_few_error(min_needed: usize, actual: usize) -> RunError {\n    let message = format!(\"not enough values to unpack (expected at least {min_needed}, got {actual})\");\n    SimpleException::new_msg(ExcType::ValueError, message).into()\n}\n\n/// Creates the appropriate ValueError for unpacking size mismatches.\n///\n/// Python uses different messages depending on whether there are too few or too many values:\n/// - Too few: \"not enough values to unpack (expected X, got Y)\"\n/// - Too many: \"too many values to unpack (expected X, got Y)\"\nfn unpack_size_error(expected: usize, actual: usize) -> RunError {\n    let message = if actual < expected {\n        format!(\"not enough values to unpack (expected {expected}, got {actual})\")\n    } else {\n        format!(\"too many values to unpack (expected {expected}, got {actual})\")\n    };\n    SimpleException::new_msg(ExcType::ValueError, message).into()\n}\n\n/// Creates a TypeError for attempting to unpack a non-iterable type.\nfn unpack_type_error(type_name: Type) -> RunError {\n    SimpleException::new_msg(\n        ExcType::TypeError,\n        format!(\"cannot unpack non-iterable {type_name} object\"),\n    )\n    .into()\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/compare.rs",
    "content": "//! Comparison operation helpers for the VM.\n\nuse super::VM;\nuse crate::{\n    defer_drop,\n    exception_private::{ExcType, RunError},\n    resource::ResourceTracker,\n    types::{LongInt, PyTrait},\n    value::Value,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Equality comparison.\n    pub(super) fn compare_eq(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        let result = lhs.py_eq(rhs, this)?;\n        this.push(Value::Bool(result));\n        Ok(())\n    }\n\n    /// Inequality comparison.\n    pub(super) fn compare_ne(&mut self) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        let result = !lhs.py_eq(rhs, this)?;\n        this.push(Value::Bool(result));\n        Ok(())\n    }\n\n    /// Ordering comparison with a predicate.\n    pub(super) fn compare_ord<F>(&mut self, check: F) -> Result<(), RunError>\n    where\n        F: FnOnce(std::cmp::Ordering) -> bool,\n    {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        let result = lhs.py_cmp(rhs, this)?.is_some_and(check);\n        this.push(Value::Bool(result));\n        Ok(())\n    }\n\n    /// Identity comparison (is/is not).\n    ///\n    /// Compares identity using `Value::is()` which compares IDs.\n    ///\n    /// Identity is determined by `Value::id()` which uses:\n    /// - Fixed IDs for singletons (None, True, False, Ellipsis)\n    /// - Interned string/bytes index for InternString/InternBytes\n    /// - HeapId for heap-allocated values (Ref)\n    /// - Value-based hashing for immediate types (Int, Float, Function, etc.)\n    pub(super) fn compare_is(&mut self, negate: bool) {\n        let this = self;\n\n        let rhs = this.pop();\n        defer_drop!(rhs, this);\n        let lhs = this.pop();\n        defer_drop!(lhs, this);\n\n        let result = lhs.is(rhs);\n        this.push(Value::Bool(if negate { !result } else { result }));\n    }\n\n    /// Membership test (in/not in).\n    pub(super) fn compare_in(&mut self, negate: bool) -> Result<(), RunError> {\n        let this = self;\n\n        let container = this.pop(); // container (rhs)\n        defer_drop!(container, this);\n        let item = this.pop(); // item to find (lhs)\n        defer_drop!(item, this);\n\n        let contained = container.py_contains(item, this)?;\n        this.push(Value::Bool(if negate { !contained } else { contained }));\n        Ok(())\n    }\n\n    /// Modulo equality comparison: a % b == k\n    ///\n    /// This is an optimization for patterns like `x % 3 == 0`. The constant k\n    /// is provided by the caller (fetched from the constant pool using the\n    /// cached code reference in the run loop).\n    ///\n    /// Uses a fast path for Int/Float types via `py_mod_eq`, and falls back to\n    /// computing `py_mod` then comparing with `py_eq` for other types (e.g., LongInt).\n    pub(super) fn compare_mod_eq(&mut self, k: &Value) -> Result<(), RunError> {\n        let this = self;\n\n        let rhs = this.pop(); // divisor (b)\n        defer_drop!(rhs, this);\n        let lhs = this.pop(); // dividend (a)\n        defer_drop!(lhs, this);\n\n        // Try fast path for Int/Float types\n        let mod_result = match k {\n            Value::Int(k_val) => lhs.py_mod_eq(rhs, *k_val),\n            _ => None,\n        };\n\n        if let Some(is_equal) = mod_result {\n            // Fast path succeeded\n            this.push(Value::Bool(is_equal));\n            Ok(())\n        } else {\n            // Fallback: compute py_mod then compare with py_eq\n            // This handles LongInt and other Ref types\n            let mod_value = lhs.py_mod(rhs, this);\n\n            match mod_value {\n                Ok(Some(v)) => {\n                    defer_drop!(v, this);\n\n                    // Handle InternLongInt by converting to heap LongInt for comparison\n                    let k_value = if let Value::InternLongInt(id) = k {\n                        let bi = this.interns.get_long_int(*id).clone();\n                        LongInt::new(bi).into_value(this.heap)?\n                    } else {\n                        // k is from the constant pool and is always an immediate value\n                        k.clone_immediate()\n                    };\n                    defer_drop!(k_value, this);\n\n                    let is_equal = v.py_eq(k_value, this)?;\n                    this.push(Value::Bool(is_equal));\n                    Ok(())\n                }\n                Ok(None) => Err(ExcType::type_error(\"unsupported operand type(s) for %\")),\n                Err(e) => Err(e),\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/exceptions.rs",
    "content": "//! Exception handling helpers for the VM.\n\nuse super::VM;\nuse crate::{\n    builtins::Builtins,\n    defer_drop,\n    exception_private::{ExcType, ExceptionRaise, RawStackFrame, RunError, SimpleException},\n    heap::{HeapData, HeapGuard},\n    intern::{StaticStrings, StringId},\n    resource::ResourceTracker,\n    types::{PyTrait, Type},\n    value::Value,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Returns the current frame's name for traceback generation.\n    ///\n    /// Returns the function name for user-defined functions, or `<module>` for\n    /// module-level code.\n    fn current_frame_name(&self) -> StringId {\n        let frame = self.current_frame();\n        match frame.function_id {\n            Some(func_id) => self.interns.get_function(func_id).name.name_id,\n            None => StaticStrings::Module.into(),\n        }\n    }\n\n    /// Creates a `RawStackFrame` for the current execution point.\n    ///\n    /// Used when raising exceptions to capture traceback information.\n    fn make_stack_frame(&self) -> RawStackFrame {\n        RawStackFrame::new(self.current_position(), self.current_frame_name(), None)\n    }\n\n    /// Attaches initial frame information to an error if it doesn't have any.\n    ///\n    /// Only sets the innermost frame if the exception doesn't already have one.\n    /// Caller frames are added separately during exception propagation.\n    ///\n    /// Uses the `hide_caret` flag from `ExceptionRaise` to determine whether to show\n    /// the caret marker in the traceback. This flag is set by error creators that know\n    /// whether CPython would show a caret for this specific error type.\n    fn attach_frame_to_error(&self, error: RunError) -> RunError {\n        match error {\n            RunError::Exc(mut exc) => {\n                if exc.frame.is_none() {\n                    let mut frame = self.make_stack_frame();\n                    // Use the hide_caret flag from the error (set by error creators)\n                    frame.hide_caret = exc.hide_caret;\n                    exc.frame = Some(frame);\n                }\n                RunError::Exc(exc)\n            }\n            RunError::UncatchableExc(mut exc) => {\n                if exc.frame.is_none() {\n                    let mut frame = self.make_stack_frame();\n                    frame.hide_caret = exc.hide_caret;\n                    exc.frame = Some(frame);\n                }\n                RunError::UncatchableExc(exc)\n            }\n            RunError::Internal(_) => error,\n        }\n    }\n\n    /// Creates a RunError from a Value that should be an exception.\n    ///\n    /// Takes ownership of the exception value and drops it properly.\n    /// The `is_raise` flag indicates if this is from a `raise` statement (hide caret).\n    pub(super) fn make_exception(&mut self, exc_value: Value, is_raise: bool) -> RunError {\n        let this = self;\n        defer_drop!(exc_value, this);\n\n        let simple_exc = match exc_value {\n            // Exception instance on heap\n            Value::Ref(heap_id) => {\n                if let HeapData::Exception(exc) = this.heap.get(*heap_id) {\n                    // Clone the exception (guard handles cleanup at scope exit)\n                    exc.clone()\n                } else {\n                    // Not an exception type\n                    SimpleException::new_msg(ExcType::TypeError, \"exceptions must derive from BaseException\")\n                }\n            }\n            // Exception type (e.g., `raise ValueError` instead of `raise ValueError()`)\n            // Instantiate with no message\n            Value::Builtin(Builtins::ExcType(exc_type)) => SimpleException::new_none(*exc_type),\n            // Invalid exception value\n            _ => SimpleException::new_msg(ExcType::TypeError, \"exceptions must derive from BaseException\"),\n        };\n\n        // Create frame with appropriate hide_caret setting\n        let frame = if is_raise {\n            RawStackFrame::from_raise(this.current_position(), this.current_frame_name())\n        } else {\n            this.make_stack_frame()\n        };\n\n        RunError::Exc(ExceptionRaise {\n            exc: simple_exc,\n            frame: Some(frame),\n            hide_caret: false,\n        })\n    }\n\n    /// Handles an exception by searching for a handler in the exception table.\n    ///\n    /// Returns:\n    /// - `Some(VMResult)` if the exception was not caught (should return from run loop)\n    /// - `None` if the exception was caught (continue execution)\n    ///\n    /// When an exception is caught:\n    /// 1. Unwinds the stack to the handler's expected depth\n    /// 2. Pushes the exception value onto the stack\n    /// 3. Sets `current_exception` for bare `raise`\n    /// 4. Jumps to the handler code\n    pub(super) fn handle_exception(&mut self, mut error: RunError) -> Option<RunError> {\n        // Ensure exception has initial frame info\n        error = self.attach_frame_to_error(error);\n\n        // For uncatchable exceptions (ResourceError like RecursionError),\n        // we still need to unwind the stack to collect all frames for the traceback\n        if matches!(error, RunError::UncatchableExc(_) | RunError::Internal(_)) {\n            return Some(self.unwind_for_traceback(error));\n        }\n\n        // Only catchable exceptions can be handled\n        let exc_info = match &error {\n            RunError::Exc(exc) => exc.clone(),\n            RunError::UncatchableExc(_) | RunError::Internal(_) => unreachable!(),\n        };\n\n        // Create exception value to push on stack\n        let exc_value = self.create_exception_value(&exc_info);\n        let exc_value = match exc_value {\n            Ok(v) => v,\n            Err(e) => return Some(e),\n        };\n\n        // Use HeapGuard because exc_value is conditionally consumed (pushed onto\n        // exception_stack when handler found) or dropped (when no handler found)\n        let mut exc_guard = HeapGuard::new(exc_value, self);\n\n        // Search for handler in current and outer frames\n        loop {\n            let (exc_value, this) = exc_guard.as_parts();\n            let frame = this.current_frame();\n            let ip = u32::try_from(this.instruction_ip).expect(\"instruction IP exceeds u32\");\n\n            // Search exception table for a handler covering this IP\n            if let Some(entry) = frame.code.find_exception_handler(ip) {\n                // Found a handler! Unwind stack and jump to it.\n                let handler_offset = usize::try_from(entry.handler()).expect(\"handler offset exceeds usize\");\n                let target_stack_depth = frame.stack_base + frame.locals_count as usize + entry.stack_depth() as usize;\n\n                // Unwind stack to target depth (drop excess values)\n                while this.stack.len() > target_stack_depth {\n                    let value = this.stack.pop().unwrap();\n                    value.drop_with_heap(this);\n                }\n\n                // Push exception value onto stack (handler expects it)\n                let exc_for_stack = exc_value.clone_with_heap(this.heap);\n                this.push(exc_for_stack);\n\n                // Reclaim exc_value from guard - it's being pushed onto exception_stack\n                let (exc_value, this) = exc_guard.into_parts();\n\n                // Push exception onto the exception_stack for bare raise\n                // This allows nested except handlers to restore outer exception context\n                this.exception_stack.push(exc_value);\n\n                // Jump to handler\n                this.current_frame_mut().ip = handler_offset;\n\n                return None; // Continue execution at handler\n            }\n\n            // No handler in this frame - pop frame and try outer\n            if this.frames.len() <= 1 {\n                // No more frames - exception is unhandled\n                let is_spawned = this.is_spawned_task();\n\n                // Drop exc_value before potentially switching tasks\n                drop(exc_guard);\n\n                // For spawned tasks, fail the task instead of propagating\n                if is_spawned {\n                    match self.handle_task_failure(error) {\n                        Ok(()) => {\n                            // Switched to next task - continue execution\n                            return None;\n                        }\n                        Err(waiter_error) => {\n                            // Switched to waiter - handle error in waiter's context\n                            return self.handle_exception(waiter_error);\n                        }\n                    }\n                }\n\n                return Some(error);\n            }\n\n            // Get the call site position before popping frame\n            // This is where the caller invoked the function that's failing\n            let call_position = this.current_frame().call_position;\n\n            // Pop this frame\n            if this.pop_frame() {\n                // The frame indicated evaluation should stop - e.g. inside `evaluate_function` - return the error\n                // now to stop unwinding.\n                return Some(error);\n            }\n\n            // Add caller frame info to traceback (if we have call position)\n            if let Some(pos) = call_position {\n                let frame_name = this.current_frame_name();\n                match &mut error {\n                    RunError::Exc(exc) => exc.add_caller_frame(pos, frame_name),\n                    RunError::UncatchableExc(exc) => exc.add_caller_frame(pos, frame_name),\n                    RunError::Internal(_) => {}\n                }\n            }\n        }\n    }\n\n    /// Unwinds the call stack to collect all frames for a traceback.\n    ///\n    /// Used for uncatchable exceptions (like RecursionError) that can't be handled\n    /// but still need a complete traceback showing all active call frames.\n    fn unwind_for_traceback(&mut self, mut error: RunError) -> RunError {\n        // Pop frames and add caller frame info to the traceback\n        while self.frames.len() > 1 {\n            // Get the call site position before popping frame\n            let call_position = self.current_frame().call_position;\n\n            // Pop this frame (cleans up namespace, etc.)\n            self.pop_frame();\n\n            // Add caller frame info to traceback\n            if let Some(pos) = call_position {\n                let frame_name = self.current_frame_name();\n                match &mut error {\n                    RunError::Exc(exc) => exc.add_caller_frame(pos, frame_name),\n                    RunError::UncatchableExc(exc) => exc.add_caller_frame(pos, frame_name),\n                    RunError::Internal(_) => {}\n                }\n            }\n        }\n        error\n    }\n\n    /// Creates an exception Value from exception info.\n    ///\n    /// Allocates an Exception on the heap and returns a Value::Ref to it.\n    fn create_exception_value(&mut self, exc: &ExceptionRaise) -> Result<Value, RunError> {\n        let exception = exc.exc.clone();\n        let heap_id = self.heap.allocate(HeapData::Exception(exception))?;\n        Ok(Value::Ref(heap_id))\n    }\n\n    /// Checks if an exception matches an exception type for except clause matching.\n    ///\n    /// Validates that `exc_type` is a valid exception type (ExcType or tuple of ExcTypes).\n    /// Returns `Ok(true)` if exception matches, `Ok(false)` if not, or `Err` if exc_type is invalid.\n    pub(super) fn check_exc_match(&self, exception: &Value, exc_type: &Value) -> Result<bool, RunError> {\n        let exc_type_enum = exception.py_type(self.heap);\n        self.check_exc_match_inner(exc_type_enum, exc_type)\n    }\n\n    /// Inner recursive helper for check_exc_match that handles tuples.\n    fn check_exc_match_inner(&self, exc_type_enum: Type, exc_type: &Value) -> Result<bool, RunError> {\n        match exc_type {\n            // Valid exception type\n            Value::Builtin(Builtins::ExcType(handler_type)) => {\n                // Check if exception is an instance of handler_type\n                Ok(matches!(exc_type_enum, Type::Exception(et) if et.is_subclass_of(*handler_type)))\n            }\n            // Tuple of exception types\n            Value::Ref(id) => {\n                if let HeapData::Tuple(tuple) = self.heap.get(*id) {\n                    for v in tuple.as_slice() {\n                        if self.check_exc_match_inner(exc_type_enum, v)? {\n                            return Ok(true);\n                        }\n                    }\n                    Ok(false)\n                } else {\n                    // Not a tuple - invalid exception type\n                    Err(ExcType::except_invalid_type_error())\n                }\n            }\n            // Any other type is invalid for except clause\n            _ => Err(ExcType::except_invalid_type_error()),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/format.rs",
    "content": "//! F-string and value formatting helpers for the VM.\n\nuse super::VM;\nuse crate::{\n    defer_drop,\n    exception_private::{ExcType, RunError, SimpleException},\n    fstring::{ParsedFormatSpec, ascii_escape, decode_format_spec, format_string, format_with_spec},\n    resource::{ResourceTracker, check_repeat_size},\n    types::{PyTrait, str::allocate_string},\n    value::Value,\n};\n\nimpl<T: ResourceTracker> VM<'_, '_, T> {\n    /// Builds an f-string by concatenating n string parts from the stack.\n    pub(super) fn build_fstring(&mut self, count: usize) -> Result<(), RunError> {\n        let parts = self.pop_n(count);\n        let mut result = String::new();\n\n        for part in parts {\n            // Each part should be a string (interned or heap-allocated)\n            let part_str = part.py_str(self);\n            result.push_str(&part_str);\n            part.drop_with_heap(self);\n        }\n\n        let value = allocate_string(result, self.heap)?;\n        self.push(value);\n        Ok(())\n    }\n\n    /// Formats a value for f-string interpolation.\n    ///\n    /// Flags encoding:\n    /// - bits 0-1: conversion (0=none, 1=str, 2=repr, 3=ascii)\n    /// - bit 2: has format spec on stack\n    ///\n    /// Python f-string formatting order:\n    /// 1. Apply format spec to original value (type-specific formatting)\n    /// 2. Apply conversion flag to the result\n    ///\n    /// However, conversion flags like !s, !r, !a are applied BEFORE formatting\n    /// if the value would be repr'd. The key insight is:\n    /// - No conversion: format the original value type\n    /// - !s conversion: convert to str first, then format as string\n    /// - !r conversion: convert to repr first, then format as string\n    /// - !a conversion: convert to ascii repr first, then format as string\n    pub(super) fn format_value(&mut self, flags: u8) -> Result<(), RunError> {\n        let this = self;\n        let conversion = flags & 0x03;\n        let has_format_spec = (flags & 0x04) != 0;\n\n        // Pop format spec if present (pushed before value, so popped after)\n        let format_spec = if has_format_spec { Some(this.pop()) } else { None };\n\n        let value = this.pop();\n        defer_drop!(value, this);\n\n        // Format with spec applied to original value type, or convert and format as string\n        let formatted = if let Some(spec_value) = format_spec {\n            defer_drop!(spec_value, this);\n\n            let spec = this.get_format_spec(spec_value, value)?;\n\n            // Pre-check: reject format specs with huge width before pad_string\n            // allocates an untracked Rust String.\n            check_repeat_size(spec.width, spec.fill.len_utf8(), this.heap.tracker())?;\n\n            match conversion {\n                // No conversion - format original value\n                0 => format_with_spec(value, &spec, this)?,\n                // !s - convert to str, format as string\n                1 => {\n                    let s = value.py_str(this);\n                    format_string(&s, &spec)?\n                }\n                // !r - convert to repr, format as string\n                2 => {\n                    let s = value.py_repr(this);\n                    format_string(&s, &spec)?\n                }\n                // !a - convert to ascii, format as string\n                3 => {\n                    let s = ascii_escape(&value.py_repr(this));\n                    format_string(&s, &spec)?\n                }\n                _ => format_with_spec(value, &spec, this)?,\n            }\n        } else {\n            // No format spec - just convert based on conversion flag\n            match conversion {\n                0 => value.py_str(this).into_owned(),\n                1 => value.py_str(this).into_owned(),\n                2 => value.py_repr(this).into_owned(),\n                3 => ascii_escape(&value.py_repr(this)),\n                _ => value.py_str(this).into_owned(),\n            }\n        };\n\n        let result = allocate_string(formatted, this.heap)?;\n        this.push(result);\n        Ok(())\n    }\n\n    /// Gets a ParsedFormatSpec from a format spec value.\n    ///\n    /// The `value_for_error` parameter is used to include the value type in error messages.\n    /// Uses lazy type capture: only calls `py_type()` in error paths.\n    fn get_format_spec(&self, spec_value: &Value, value_for_error: &Value) -> Result<ParsedFormatSpec, RunError> {\n        match spec_value {\n            Value::Int(n) if *n < 0 => {\n                // Decode the encoded format spec; n < 0 ensures (-n - 1) >= 0\n                let encoded = u64::try_from((-*n) - 1).expect(\"format spec encoding validated non-negative\");\n                Ok(decode_format_spec(encoded))\n            }\n            _ => {\n                // Dynamic format spec - parse the string\n                let spec_str = spec_value.py_str(self);\n                spec_str.parse::<ParsedFormatSpec>().map_err(|invalid| {\n                    // Only fetch type in error path\n                    let value_type = value_for_error.py_type(self.heap);\n                    RunError::Exc(\n                        SimpleException::new_msg(\n                            ExcType::ValueError,\n                            format!(\"Invalid format specifier '{invalid}' for object of type '{value_type}'\"),\n                        )\n                        .into(),\n                    )\n                })\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/mod.rs",
    "content": "//! Bytecode virtual machine for executing compiled Python code.\n//!\n//! The VM uses a stack-based execution model with an operand stack for computation\n//! and a call stack for function frames. Each frame owns its instruction pointer (IP).\n\nmod async_exec;\nmod attr;\nmod binary;\nmod call;\nmod collections;\nmod compare;\nmod exceptions;\nmod format;\nmod scheduler;\n\nuse std::cmp::Ordering;\n\npub(crate) use call::CallResult;\nuse scheduler::Scheduler;\n\nuse crate::{\n    MontyObject,\n    args::ArgValues,\n    asyncio::{CallId, TaskId},\n    bytecode::{code::Code, op::Opcode},\n    exception_private::{ExcType, RunError, RunResult, SimpleException},\n    heap::{ContainsHeap, DropWithHeap, Heap, HeapData, HeapGuard, HeapId},\n    heap_data::{Closure, FunctionDefaults, HeapDataMut},\n    intern::{FunctionId, Interns, StringId},\n    io::PrintWriter,\n    modules::BuiltinModule,\n    os::OsFunction,\n    parse::CodeRange,\n    resource::ResourceTracker,\n    types::{LongInt, MontyIter, PyTrait, iter::advance_on_heap},\n    value::{BitwiseOp, EitherStr, Value},\n};\n\n/// Result of executing Await opcode.\n///\n/// Indicates what the VM should do after awaiting a value:\n/// - `ValueReady`: the awaited value resolved immediately, push it\n/// - `FramePushed`: a new frame was pushed for coroutine execution\n/// - `Yield`: all tasks blocked, yield to caller with pending futures\nenum AwaitResult {\n    /// The awaited value resolved immediately (e.g., resolved ExternalFuture).\n    ValueReady(Value),\n    /// A new frame was pushed to execute a coroutine.\n    FramePushed,\n    /// All tasks are blocked - yield to caller with pending futures.\n    Yield(Vec<CallId>),\n}\n\n/// Tries an operation and handles exceptions, reloading cached frame state.\n///\n/// Use this in the main run loop where `cached_frame`\n/// are used. After catching an exception, reloads the cache since the handler\n/// may be in a different frame.\nmacro_rules! try_catch_sync {\n    ($self:expr, $cached_frame:ident, $expr:expr) => {\n        if let Err(e) = $expr {\n            if let Some(result) = $self.handle_exception(e) {\n                return Err(result);\n            }\n            // Exception was caught - handler may be in different frame, reload cache\n            reload_cache!($self, $cached_frame);\n        }\n    };\n}\n\n/// Handles an exception and reloads cached frame state if caught.\n///\n/// Use this in the main run loop where `cached_frame`\n/// are used. After catching an exception, reloads the cache since the handler\n/// may be in a different frame.\n///\n/// Wrapped in a block to allow use in match arm expressions.\nmacro_rules! catch_sync {\n    ($self:expr, $cached_frame:ident, $err:expr) => {{\n        if let Some(result) = $self.handle_exception($err) {\n            return Err(result);\n        }\n        // Exception was caught - handler may be in different frame, reload cache\n        reload_cache!($self, $cached_frame);\n    }};\n}\n\n/// Fetches a byte from bytecode using cached code/ip, advancing ip.\n///\n/// Used in the run loop for fast operand fetching without frame access.\nmacro_rules! fetch_byte {\n    ($cached_frame:expr) => {{\n        let byte = $cached_frame.code.bytecode()[$cached_frame.ip];\n        $cached_frame.ip += 1;\n        byte\n    }};\n}\n\n/// Fetches a u8 operand using cached code/ip.\nmacro_rules! fetch_u8 {\n    ($cached_frame:expr) => {\n        fetch_byte!($cached_frame)\n    };\n}\n\n/// Fetches an i8 operand using cached code/ip.\nmacro_rules! fetch_i8 {\n    ($cached_frame:expr) => {{ i8::from_ne_bytes([fetch_byte!($cached_frame)]) }};\n}\n\n/// Fetches a u16 operand (little-endian) using cached code/ip.\nmacro_rules! fetch_u16 {\n    ($cached_frame:expr) => {{\n        let lo = $cached_frame.code.bytecode()[$cached_frame.ip];\n        let hi = $cached_frame.code.bytecode()[$cached_frame.ip + 1];\n        $cached_frame.ip += 2;\n        u16::from_le_bytes([lo, hi])\n    }};\n}\n\n/// Fetches an i16 operand (little-endian) using cached code/ip.\nmacro_rules! fetch_i16 {\n    ($cached_frame:expr) => {{\n        let lo = $cached_frame.code.bytecode()[$cached_frame.ip];\n        let hi = $cached_frame.code.bytecode()[$cached_frame.ip + 1];\n        $cached_frame.ip += 2;\n        i16::from_le_bytes([lo, hi])\n    }};\n}\n\n/// Reloads cached frame state from the current frame.\n///\n/// Call this after any operation that modifies the frame stack (calls, returns,\n/// exception handling).\nmacro_rules! reload_cache {\n    ($self:expr, $cached_frame:ident) => {{\n        $cached_frame = $self.new_cached_frame();\n    }};\n}\n\n/// Applies a relative jump offset to the cached IP.\n///\n/// Uses checked arithmetic to safely compute the new IP, panicking if the\n/// jump would result in a negative or overflowing instruction pointer.\nmacro_rules! jump_relative {\n    ($ip:expr, $offset:expr) => {{\n        let ip_i64 = i64::try_from($ip).expect(\"instruction pointer exceeds i64\");\n        let new_ip = ip_i64 + i64::from($offset);\n        $ip = usize::try_from(new_ip).expect(\"jump resulted in negative or overflowing IP\");\n    }};\n}\n\n/// Handles the result of a load operation that may yield a `FrameExit::NameLookup`.\n///\n/// `load_local` and `load_global` return `Result<Option<FrameExit>, RunError>`:\n/// - `Ok(None)`: load succeeded, value is on the stack\n/// - `Ok(Some(FrameExit::NameLookup { .. }))`: unresolved name, yield to host\n/// - `Err(e)`: exception (e.g., UnboundLocalError)\nmacro_rules! handle_load_result {\n    ($self:expr, $cached_frame:ident, $result:expr) => {\n        match $result {\n            Ok(None) => {}\n            Ok(Some(frame_exit)) => {\n                $self.current_frame_mut().ip = $cached_frame.ip;\n                return Ok(frame_exit);\n            }\n            Err(e) => catch_sync!($self, $cached_frame, e),\n        }\n    };\n}\n\n/// Handles the result of a call operation that returns `CallResult`.\n///\n/// This macro eliminates the repetitive pattern of matching on `CallResult`\n/// variants that appears in LoadAttr, CallFunction, CallFunctionKw, CallAttr,\n/// CallAttrKw, and CallFunctionExtended opcodes.\n///\n/// Actions taken for each variant:\n/// - `Push(value)`: Push the value onto the stack\n/// - `FramePushed`: Reload the cached frame (a new frame was pushed)\n/// - `External(ext_id, args)`: Return `FrameExit::ExternalCall` to yield to host\n/// - `OsCall(func, args)`: Return `FrameExit::OsCall` to yield to host\n/// - `MethodCall(name, args)`: Return `FrameExit::MethodCall` to yield to host\n/// - `AwaitValue(value)`: Push value, then implicitly await it via `exec_get_awaitable`\n/// - `Err(err)`: Handle the exception via `catch_sync!`\nmacro_rules! handle_call_result {\n    ($self:expr, $cached_frame:ident, $result:expr) => {\n        match $result {\n            Ok(CallResult::Value(result)) => $self.push(result),\n            Ok(CallResult::FramePushed) => reload_cache!($self, $cached_frame),\n            Ok(CallResult::External(name, args)) => {\n                let call_id = $self.allocate_call_id();\n                let name_load_ip = $self.ext_function_load_ip.take();\n                // Sync cached IP back to frame before snapshot for resume\n                $self.current_frame_mut().ip = $cached_frame.ip;\n                return Ok(FrameExit::ExternalCall {\n                    function_name: name,\n                    args,\n                    call_id,\n                    name_load_ip,\n                });\n            }\n            Ok(CallResult::OsCall(func, args)) => {\n                let call_id = $self.allocate_call_id();\n                // Sync cached IP back to frame before snapshot for resume\n                $self.current_frame_mut().ip = $cached_frame.ip;\n                return Ok(FrameExit::OsCall {\n                    function: func,\n                    args,\n                    call_id,\n                });\n            }\n            Ok(CallResult::MethodCall(method_name, args)) => {\n                let call_id = $self.allocate_call_id();\n                // Sync cached IP back to frame before snapshot for resume\n                $self.current_frame_mut().ip = $cached_frame.ip;\n                return Ok(FrameExit::MethodCall {\n                    method_name,\n                    args,\n                    call_id,\n                });\n            }\n            Ok(CallResult::AwaitValue(value)) => {\n                // Push the value and implicitly await it (used by asyncio.run())\n                $self.push(value);\n                $self.current_frame_mut().ip = $cached_frame.ip;\n                match $self.exec_get_awaitable() {\n                    Ok(AwaitResult::ValueReady(value)) => {\n                        $self.push(value);\n                    }\n                    Ok(AwaitResult::FramePushed) => {\n                        reload_cache!($self, $cached_frame);\n                    }\n                    Ok(AwaitResult::Yield(pending_calls)) => {\n                        return Ok(FrameExit::ResolveFutures(pending_calls));\n                    }\n                    Err(e) => {\n                        catch_sync!($self, $cached_frame, e);\n                    }\n                }\n            }\n            Err(err) => catch_sync!($self, $cached_frame, err),\n        }\n    };\n}\n\n/// Result of VM execution.\npub enum FrameExit {\n    /// Execution completed successfully with a return value.\n    Return(Value),\n\n    /// Execution paused for an external function call.\n    ///\n    /// The caller should execute the external function and call `resume()`\n    /// with the result. The `call_id` allows the host to use async resolution\n    /// by calling `run_pending()` instead of `run(result)`.\n    ExternalCall {\n        /// Name of the external function to call (interned or heap-owned).\n        function_name: EitherStr,\n        /// Arguments for the external function (includes both positional and keyword args).\n        args: ArgValues,\n        /// Unique ID for this call, used for async correlation.\n        call_id: CallId,\n        /// Optional bytecode IP of the load instruction that produced this `ExtFunction`.\n        ///\n        /// When a `LoadGlobalCallable`/`LoadLocalCallable` opcode auto-injects an `ExtFunction`\n        /// for an undefined name, the load instruction's IP is saved here. In standard execution\n        /// (without external function support), this IP is used to restore the frame pointer\n        /// before raising `NameError`, so the traceback points to the name rather than the call.\n        name_load_ip: Option<usize>,\n    },\n\n    /// Execution paused for an os function call.\n    ///\n    /// The caller should execute a function corresponding to the `os_call` and call `resume()`\n    /// with the result. The `call_id` allows the host to use async resolution\n    /// by calling `run_pending()` instead of `run(result)`.\n    OsCall {\n        /// ID of the os function to call.\n        function: OsFunction,\n        /// Arguments for the external function (includes both positional and keyword args).\n        args: ArgValues,\n        /// Unique ID for this call, used for async correlation.\n        call_id: CallId,\n    },\n\n    /// Execution paused for a dataclass method call.\n    ///\n    /// The caller should invoke the method on the original Python dataclass and call\n    /// `resume()` with the result. The `method_name` is the attribute name (e.g.\n    /// `\"distance\"`) and `args` includes the dataclass instance as the first argument\n    /// (`self`).\n    MethodCall {\n        /// Method name (e.g., \"distance\").\n        method_name: EitherStr,\n        /// Arguments including the dataclass instance as the first positional arg.\n        args: ArgValues,\n        /// Unique ID for this call, used for async correlation.\n        call_id: CallId,\n    },\n\n    /// All tasks are blocked waiting for external futures to resolve.\n    ///\n    /// The caller must resolve the pending CallIds before calling `resume()`.\n    /// This happens when await is called on an ExternalFuture that hasn't\n    /// been resolved yet, and there are no other ready tasks to switch to.\n    ResolveFutures(Vec<CallId>),\n\n    /// Execution paused for an unresolved name lookup.\n    ///\n    /// When the VM encounters an `Undefined` value in a `LocalUnassigned` slot\n    /// (module level) or a global slot, it yields to the host to resolve the name.\n    /// The host can return a value to cache in the slot, or indicate the name is\n    /// truly undefined (which will raise `NameError`).\n    ///\n    /// This enables auto-detection of external functions without requiring upfront\n    /// declaration: unresolved names are lazily resolved by the host at runtime.\n    NameLookup {\n        /// The interned name being looked up.\n        name_id: StringId,\n        /// The namespace slot where the resolved value should be cached.\n        namespace_slot: u16,\n        /// Whether this is a global slot (true) or a local/function slot (false).\n        is_global: bool,\n    },\n}\n\n/// A single function activation record.\n///\n/// Each frame represents one level in the call stack and owns its own\n/// instruction pointer. This design avoids sync bugs on call/return.\n#[derive(Debug)]\npub struct CallFrame<'code> {\n    /// Bytecode being executed.\n    code: &'code Code,\n\n    /// Instruction pointer within this frame's bytecode.\n    ip: usize,\n\n    /// Base index into the VM stack for this frame's locals region.\n    ///\n    /// The frame's locals occupy `stack[stack_base..stack_base + locals_count]`,\n    /// and operands are pushed above that.\n    stack_base: usize,\n\n    /// Number of local variable slots in this frame.\n    ///\n    /// Zero for module-level frames (globals are stored separately).\n    /// For function frames, this equals `func.namespace_size`.\n    locals_count: u16,\n\n    /// Function ID (for tracebacks). None for module-level code.\n    function_id: Option<FunctionId>,\n\n    /// Call site position (for tracebacks).\n    call_position: Option<CodeRange>,\n\n    /// When this frame returns (or exits with an exception) the VM should exit the run loop\n    /// and return to the caller. Supports `evaluate_function`.\n    should_return: bool,\n}\n\nimpl<'code> CallFrame<'code> {\n    /// Creates a new call frame for module-level code.\n    ///\n    /// Module frames have `locals_count = 0` because module-level variables\n    /// are stored in the VM's `globals` vec, not in the stack.\n    pub fn new_module(code: &'code Code) -> Self {\n        Self {\n            code,\n            ip: 0,\n            stack_base: 0,\n            locals_count: 0,\n            function_id: None,\n            call_position: None,\n            should_return: false,\n        }\n    }\n\n    /// Creates a new call frame for a function call.\n    ///\n    /// The frame's locals occupy `stack[stack_base..stack_base + locals_count]`.\n    /// Operands are pushed above the locals region.\n    pub fn new_function(\n        code: &'code Code,\n        stack_base: usize,\n        locals_count: u16,\n        function_id: FunctionId,\n        call_position: Option<CodeRange>,\n    ) -> Self {\n        Self {\n            code,\n            ip: 0,\n            stack_base,\n            locals_count,\n            function_id: Some(function_id),\n            call_position,\n            should_return: false,\n        }\n    }\n}\n\n/// Cached state of the VM derived from the current frame as an optimization.\n///\n/// Holds the hot fields from the current `CallFrame` to avoid repeated\n/// `frames.last()` lookups in the main opcode loop.\n#[derive(Debug, Copy, Clone)]\npub struct CachedFrame<'code> {\n    /// Bytecode being executed.\n    code: &'code Code,\n\n    /// Instruction pointer within this frame's bytecode.\n    ip: usize,\n\n    /// Base index into the VM stack for this frame's locals.\n    stack_base: usize,\n}\n\nimpl<'code> From<&CallFrame<'code>> for CachedFrame<'code> {\n    fn from(frame: &CallFrame<'code>) -> Self {\n        Self {\n            code: frame.code,\n            ip: frame.ip,\n            stack_base: frame.stack_base,\n        }\n    }\n}\n\n/// Serializable representation of a call frame.\n///\n/// Cannot store `&Code` (a reference) — instead stores `FunctionId` to look up\n/// the pre-compiled Code object on resume. Module-level code uses `None`.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct SerializedFrame {\n    /// Which function's code this frame executes (None = module-level).\n    function_id: Option<FunctionId>,\n\n    /// Instruction pointer within this frame's bytecode.\n    ip: usize,\n\n    /// Base index into the VM stack for this frame's locals region.\n    stack_base: usize,\n\n    /// Number of local variable slots (0 for module-level frames).\n    locals_count: u16,\n\n    /// Call site position (for tracebacks).\n    call_position: Option<CodeRange>,\n}\n\nimpl CallFrame<'_> {\n    /// Converts this frame to a serializable representation.\n    fn serialize(&self) -> SerializedFrame {\n        assert!(\n            !self.should_return,\n            \"cannot serialize frame marked for return - not yet supported\"\n        );\n        SerializedFrame {\n            function_id: self.function_id,\n            ip: self.ip,\n            stack_base: self.stack_base,\n            locals_count: self.locals_count,\n            call_position: self.call_position,\n        }\n    }\n}\n\n/// VM state for pause/resume at external function calls.\n///\n/// **Ownership:** This struct OWNS the values (refcounts were already incremented).\n/// Must be used with the serialized Heap - HeapId values are indices into that heap.\n///\n/// **Usage:** When the VM pauses for an external call, call `into_snapshot()` to\n/// create this snapshot. The snapshot can be serialized and stored. On resume,\n/// use `restore()` to reconstruct the VM and continue execution.\n///\n/// Note: This struct does not implement `Clone` because `Value` uses manual\n/// reference counting. Snapshots transfer ownership - they are not copied.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub struct VMSnapshot {\n    /// Operand stack — locals and operands interleaved per frame.\n    ///\n    /// Each function frame's locals occupy `stack[frame.stack_base..frame.stack_base + frame.locals_count]`,\n    /// with operands pushed above.\n    pub(crate) stack: Vec<Value>,\n\n    /// Module-level (global) variable storage.\n    pub(crate) globals: Vec<Value>,\n\n    /// Call frames (serializable form — stores FunctionId, not &Code).\n    frames: Vec<SerializedFrame>,\n\n    /// Stack of exceptions being handled for nested except blocks.\n    ///\n    /// When entering an except handler, the exception is pushed onto this stack.\n    /// When exiting via `ClearException`, the top is popped. This allows nested\n    /// except handlers to restore the outer exception context.\n    exception_stack: Vec<Value>,\n\n    /// IP of the instruction that caused the pause (for exception handling).\n    instruction_ip: usize,\n\n    /// Scheduler state (always present).\n    ///\n    /// Contains call ID counter, task state, pending calls, and resolved futures.\n    scheduler: Scheduler,\n}\n\n// ============================================================================\n// Virtual Machine\n// ============================================================================\n\n/// The bytecode virtual machine.\n///\n/// Executes compiled bytecode using a stack-based execution model.\n/// The instruction pointer (IP) lives in each `CallFrame`, not here,\n/// to avoid sync bugs on call/return.\n///\n/// # Lifetimes\n/// * `'a` - Lifetime of the heap, namespaces, and interns\n/// * `'p` - Lifetime of the print writer's internal references\npub struct VM<'a, 'p, T: ResourceTracker> {\n    /// Operand stack — locals and operands interleaved per frame.\n    ///\n    /// Each function frame's locals occupy `stack[frame.stack_base..frame.stack_base + frame.locals_count]`,\n    /// with operands pushed above. Module-level frames have `locals_count = 0`\n    /// because globals are stored separately.\n    pub(crate) stack: Vec<Value>,\n\n    /// Module-level (global) variable storage.\n    ///\n    /// Indexed by slot number from `LoadGlobal`/`StoreGlobal` opcodes.\n    /// Separated from the stack because globals persist across function calls\n    /// and are accessed via dedicated opcodes.\n    pub(crate) globals: Vec<Value>,\n\n    /// Call stack — function frames (each frame has its own IP).\n    frames: Vec<CallFrame<'a>>,\n\n    /// Heap for reference-counted objects.\n    pub(crate) heap: &'a mut Heap<T>,\n\n    /// Interned strings/bytes.\n    pub(crate) interns: &'a Interns,\n\n    /// Print output writer, borrowed so callers retain access to collected output.\n    pub(crate) print_writer: PrintWriter<'p>,\n\n    /// Stack of exceptions being handled for nested except blocks.\n    ///\n    /// Used by bare `raise` to re-raise the current exception.\n    /// When entering an except handler, the exception is pushed onto this stack.\n    /// When exiting via `ClearException`, the top is popped. This allows nested\n    /// except handlers to restore the outer exception context.\n    exception_stack: Vec<Value>,\n\n    /// IP of the instruction being executed (for exception table lookup).\n    ///\n    /// Updated at the start of each instruction before operands are fetched.\n    /// This allows us to find the correct exception handler when an error occurs.\n    instruction_ip: usize,\n\n    /// Scheduler for task management and call ID allocation.\n    ///\n    /// Always present — owns `next_call_id` (used by both sync and async paths)\n    /// plus async task state. Internal collections don't allocate until first use,\n    /// so sync-only code pays only for the main task entry.\n    scheduler: Scheduler,\n\n    /// Module-level code (for restoring main task frames).\n    ///\n    /// Stored here because the main task's frames have `function_id: None` and\n    /// need a reference to the module code when being restored after task switching.\n    module_code: Option<&'a Code>,\n\n    /// Bytecode IP of the most recent `LoadGlobalCallable`/`LoadLocalCallable` that\n    /// pushed an `ExtFunction` for an undefined name.\n    ///\n    /// Used to restore the frame IP when standard execution converts an `ExternalCall`\n    /// back to a `NameError`, so the traceback points to the name reference rather than\n    /// the call expression.\n    ext_function_load_ip: Option<usize>,\n}\n\nimpl<'a, 'p, T: ResourceTracker> VM<'a, 'p, T> {\n    /// Creates a new VM with the given runtime context.\n    pub fn new(\n        globals: Vec<Value>,\n        heap: &'a mut Heap<T>,\n        interns: &'a Interns,\n        print_writer: PrintWriter<'p>,\n    ) -> Self {\n        Self {\n            stack: Vec::with_capacity(64),\n            globals,\n            frames: Vec::with_capacity(16),\n            heap,\n            interns,\n            print_writer,\n            exception_stack: Vec::new(),\n            instruction_ip: 0,\n            scheduler: Scheduler::new(),\n            ext_function_load_ip: None, // Set by LoadGlobalCallable/LoadLocalCallable\n            module_code: None,\n        }\n    }\n\n    /// Reconstructs a VM from a snapshot.\n    ///\n    /// The heap must already be deserialized. `FunctionId` values\n    /// in frames are used to look up pre-compiled `Code` objects from the `Interns`.\n    /// The `module_code` is used for frames with `function_id = None`.\n    ///\n    /// # Arguments\n    /// * `snapshot` - The VM snapshot to restore\n    /// * `module_code` - Compiled module code (for frames with function_id = None)\n    /// * `heap` - The deserialized heap\n    /// * `interns` - Interns for looking up function code\n    /// * `print_writer` - Writer for print output\n    pub fn restore(\n        snapshot: VMSnapshot,\n        module_code: &'a Code,\n        heap: &'a mut Heap<T>,\n        interns: &'a Interns,\n        print_writer: PrintWriter<'p>,\n    ) -> Self {\n        // Reconstruct call frames from serialized form\n        let frames: Vec<CallFrame<'_>> = snapshot\n            .frames\n            .into_iter()\n            .map(|sf| {\n                let code = match sf.function_id {\n                    Some(func_id) => &interns.get_function(func_id).code,\n                    None => module_code,\n                };\n                CallFrame {\n                    code,\n                    ip: sf.ip,\n                    stack_base: sf.stack_base,\n                    locals_count: sf.locals_count,\n                    function_id: sf.function_id,\n                    call_position: sf.call_position,\n                    should_return: false,\n                }\n            })\n            .collect();\n\n        // Restore recursion depth to match the number of active function frames.\n        // During serialization, recursion_depth is transient (defaults to 0),\n        // but cleanup paths call decr_recursion_depth for each non-root frame.\n        let current_frame_depth = frames.len().saturating_sub(1); // Subtract 1 for root frame which doesn't contribute to depth\n        heap.set_recursion_depth(current_frame_depth);\n\n        Self {\n            stack: snapshot.stack,\n            globals: snapshot.globals,\n            frames,\n            heap,\n            interns,\n            print_writer,\n            exception_stack: snapshot.exception_stack,\n            instruction_ip: snapshot.instruction_ip,\n            scheduler: snapshot.scheduler,\n            module_code: Some(module_code),\n            ext_function_load_ip: None,\n        }\n    }\n    /// Consumes the VM and creates a snapshot for pause/resume.\n    ///\n    /// **Ownership transfer:** This method takes `self` by value, consuming the VM.\n    /// The snapshot owns all Values (refcounts already correct from the live VM).\n    /// The heap and namespaces must be serialized alongside this snapshot.\n    ///\n    /// This is NOT a clone - it's a transfer. After calling this, the original VM\n    /// is gone and only the snapshot (+ serialized heap/namespaces) represents the state.\n    pub fn snapshot(self) -> VMSnapshot {\n        VMSnapshot {\n            // Move values directly — no clone, no refcount increment needed\n            // (the VM owned them, now the snapshot owns them)\n            stack: self.stack,\n            globals: self.globals,\n            frames: self.frames.into_iter().map(|f| f.serialize()).collect(),\n            exception_stack: self.exception_stack,\n            instruction_ip: self.instruction_ip,\n            scheduler: self.scheduler,\n        }\n    }\n\n    /// Pushes an initial frame for module-level code and runs the VM.\n    pub fn run_module(&mut self, code: &'a Code) -> Result<FrameExit, RunError> {\n        // Store module code for restoring main task frames during task switching\n        self.module_code = Some(code);\n        self.push_frame(CallFrame::new_module(code))?;\n        self.run()\n    }\n\n    /// Cleans up VM state before the VM is dropped.\n    ///\n    /// This method must be called before the VM goes out of scope to ensure\n    /// proper reference counting cleanup for any exception values and scheduler state.\n    pub fn cleanup(&mut self) {\n        // Drop all exceptions in the exception stack\n        self.exception_stack.drain(..).drop_with_heap(self.heap);\n        // Clean up current task's stack values and frame cell references\n        self.cleanup_current_task();\n        // Clean up scheduler state (task stacks, pending calls, resolved values, frame cells)\n        self.scheduler.cleanup(self.heap);\n        self.globals.drain(..).drop_with_heap(self.heap);\n    }\n\n    /// Returns the `stack_base` of the current (topmost) call frame.\n    ///\n    /// Used by `NameLookup` resolution to determine which stack region to cache\n    /// resolved values into when the lookup originated from a function scope.\n    pub fn current_stack_base(&self) -> usize {\n        self.frames\n            .last()\n            .expect(\"VM should have at least one frame\")\n            .stack_base\n    }\n\n    /// Takes ownership of the globals vector, replacing it with an empty vec.\n    ///\n    /// Used by the REPL to reclaim globals after VM execution completes,\n    /// before calling `cleanup()` (which would destroy them in ref-count-panic mode).\n    pub fn take_globals(&mut self) -> Vec<Value> {\n        std::mem::take(&mut self.globals)\n    }\n\n    /// Allocates a new `CallId` for an external function call.\n    fn allocate_call_id(&mut self) -> CallId {\n        self.scheduler.allocate_call_id()\n    }\n\n    /// Returns true if we're on the main task (or no async at all).\n    ///\n    /// This is used to determine whether a `ReturnValue` at the last frame means\n    /// module-level completion (return to host) or spawned task completion\n    /// (handle task completion and switch).\n    fn is_main_task(&self) -> bool {\n        self.scheduler.current_task_id().is_none_or(TaskId::is_main)\n    }\n\n    /// Main execution loop.\n    ///\n    /// Fetches opcodes from the current frame's bytecode and executes them.\n    /// Returns when execution completes, an error occurs, or an external\n    /// call is needed.\n    ///\n    /// Uses locally cached `code` and `ip` variables to avoid repeated\n    /// `frames.last_mut().expect()` calls during operand fetching. The cache\n    /// is reloaded after any operation that modifies the frame stack.\n    pub fn run(&mut self) -> Result<FrameExit, RunError> {\n        // Cache frame state locally to avoid repeated frames.last_mut() calls.\n        // The Code reference has lifetime 'a (lives in Interns), independent of frame borrow.\n        let mut cached_frame: CachedFrame<'a> = self.new_cached_frame();\n\n        loop {\n            // Check time limit and trigger GC if needed at each instruction.\n            // For NoLimitTracker, these are inlined no-ops that compile away.\n            self.heap.check_time()?;\n\n            if self.heap.should_gc() {\n                // Sync IP before GC for safety\n                self.current_frame_mut().ip = cached_frame.ip;\n                self.run_gc();\n            }\n\n            // Track instruction IP for exception table lookup\n            self.instruction_ip = cached_frame.ip;\n\n            // Fetch opcode using cached values (no frame access)\n            let opcode = {\n                let byte = cached_frame.code.bytecode()[cached_frame.ip];\n                cached_frame.ip += 1;\n                Opcode::try_from(byte).expect(\"invalid opcode in bytecode\")\n            };\n\n            match opcode {\n                // ============================================================\n                // Stack Operations\n                // ============================================================\n                Opcode::Pop => {\n                    let value = self.pop();\n                    value.drop_with_heap(self);\n                }\n                Opcode::Dup => {\n                    let value = self.peek().clone_with_heap(self);\n                    self.push(value);\n                }\n                Opcode::Dup2 => {\n                    let len = self.stack.len();\n                    let first = self.stack[len - 2].clone_with_heap(self);\n                    let second = self.stack[len - 1].clone_with_heap(self);\n                    self.push(first);\n                    self.push(second);\n                }\n                Opcode::Rot2 => {\n                    // Swap top two: [a, b] → [b, a]\n                    let len = self.stack.len();\n                    self.stack.swap(len - 1, len - 2);\n                }\n                Opcode::Rot3 => {\n                    // Rotate top three: [a, b, c] → [c, a, b]\n                    // Uses in-place rotation without cloning\n                    let len = self.stack.len();\n                    // Move c out, then shift a→b→c, then put c at a's position\n                    // Equivalent to: [..rest, a, b, c] → [..rest, c, a, b]\n                    self.stack[len - 3..].rotate_right(1);\n                }\n                // Constants & Literals\n                Opcode::LoadConst => {\n                    let idx = fetch_u16!(cached_frame);\n                    let value = cached_frame.code.constants().get(idx);\n                    // Handle InternLongInt specially - convert to heap-allocated LongInt\n                    if let Value::InternLongInt(long_int_id) = value {\n                        let bi = self.interns.get_long_int(*long_int_id).clone();\n                        match LongInt::new(bi).into_value(self.heap) {\n                            Ok(v) => self.push(v),\n                            Err(e) => catch_sync!(self, cached_frame, RunError::from(e)),\n                        }\n                    } else {\n                        self.push(value.clone_with_heap(self));\n                    }\n                }\n                Opcode::LoadNone => self.push(Value::None),\n                Opcode::LoadTrue => self.push(Value::Bool(true)),\n                Opcode::LoadFalse => self.push(Value::Bool(false)),\n                Opcode::LoadSmallInt => {\n                    let n = fetch_i8!(cached_frame);\n                    self.push(Value::Int(i64::from(n)));\n                }\n                // Variables - Specialized Local Loads (no operand)\n                Opcode::LoadLocal0 => handle_load_result!(self, cached_frame, self.load_local(&cached_frame, 0)),\n                Opcode::LoadLocal1 => handle_load_result!(self, cached_frame, self.load_local(&cached_frame, 1)),\n                Opcode::LoadLocal2 => handle_load_result!(self, cached_frame, self.load_local(&cached_frame, 2)),\n                Opcode::LoadLocal3 => handle_load_result!(self, cached_frame, self.load_local(&cached_frame, 3)),\n                // Variables - General Local Operations\n                Opcode::LoadLocal => {\n                    let slot = u16::from(fetch_u8!(cached_frame));\n                    handle_load_result!(self, cached_frame, self.load_local(&cached_frame, slot));\n                }\n                Opcode::LoadLocalW => {\n                    let slot = fetch_u16!(cached_frame);\n                    handle_load_result!(self, cached_frame, self.load_local(&cached_frame, slot));\n                }\n                Opcode::StoreLocal => {\n                    let slot = u16::from(fetch_u8!(cached_frame));\n                    self.store_local(&cached_frame, slot);\n                }\n                Opcode::StoreLocalW => {\n                    let slot = fetch_u16!(cached_frame);\n                    self.store_local(&cached_frame, slot);\n                }\n                Opcode::DeleteLocal => {\n                    let slot = u16::from(fetch_u8!(cached_frame));\n                    self.delete_local(&cached_frame, slot);\n                }\n                Opcode::DeleteGlobal => {\n                    let slot = fetch_u16!(cached_frame);\n                    self.delete_global(slot);\n                }\n                // Variables - Callable-context Local Loads\n                Opcode::LoadLocalCallable => {\n                    let slot = u16::from(fetch_u8!(cached_frame));\n                    let name_id = StringId::from_index(fetch_u16!(cached_frame));\n                    self.load_local_callable(&cached_frame, slot, name_id);\n                }\n                Opcode::LoadLocalCallableW => {\n                    let slot = fetch_u16!(cached_frame);\n                    let name_id = StringId::from_index(fetch_u16!(cached_frame));\n                    self.load_local_callable(&cached_frame, slot, name_id);\n                }\n                // Variables - Global Operations\n                Opcode::LoadGlobal => {\n                    let slot = fetch_u16!(cached_frame);\n                    handle_load_result!(self, cached_frame, self.load_global(slot));\n                }\n                Opcode::LoadGlobalCallable => {\n                    let slot = fetch_u16!(cached_frame);\n                    let name_id = StringId::from_index(fetch_u16!(cached_frame));\n                    self.load_global_callable(slot, name_id);\n                }\n                Opcode::StoreGlobal => {\n                    let slot = fetch_u16!(cached_frame);\n                    self.store_global(slot);\n                }\n                // Variables - Cell Operations (closures)\n                Opcode::LoadCell => {\n                    let slot = fetch_u16!(cached_frame);\n                    try_catch_sync!(self, cached_frame, self.load_cell(&cached_frame, slot));\n                }\n                Opcode::StoreCell => {\n                    let slot = fetch_u16!(cached_frame);\n                    self.store_cell(&cached_frame, slot);\n                }\n                // Binary Operations - route through exception handling for tracebacks\n                Opcode::BinaryAdd => try_catch_sync!(self, cached_frame, self.binary_add()),\n                Opcode::BinarySub => try_catch_sync!(self, cached_frame, self.binary_sub()),\n                Opcode::BinaryMul => try_catch_sync!(self, cached_frame, self.binary_mult()),\n                Opcode::BinaryDiv => try_catch_sync!(self, cached_frame, self.binary_div()),\n                Opcode::BinaryFloorDiv => try_catch_sync!(self, cached_frame, self.binary_floordiv()),\n                Opcode::BinaryMod => try_catch_sync!(self, cached_frame, self.binary_mod()),\n                Opcode::BinaryPow => try_catch_sync!(self, cached_frame, self.binary_pow()),\n                // Bitwise operations - only work on integers\n                Opcode::BinaryAnd => try_catch_sync!(self, cached_frame, self.binary_and()),\n                Opcode::BinaryOr => try_catch_sync!(self, cached_frame, self.binary_or()),\n                Opcode::BinaryXor => try_catch_sync!(self, cached_frame, self.binary_xor()),\n                Opcode::BinaryLShift => {\n                    try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::LShift));\n                }\n                Opcode::BinaryRShift => {\n                    try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::RShift));\n                }\n                Opcode::BinaryMatMul => try_catch_sync!(self, cached_frame, self.binary_matmul()),\n                // Comparison Operations\n                Opcode::CompareEq => try_catch_sync!(self, cached_frame, self.compare_eq()),\n                Opcode::CompareNe => try_catch_sync!(self, cached_frame, self.compare_ne()),\n                Opcode::CompareLt => try_catch_sync!(self, cached_frame, self.compare_ord(Ordering::is_lt)),\n                Opcode::CompareLe => try_catch_sync!(self, cached_frame, self.compare_ord(Ordering::is_le)),\n                Opcode::CompareGt => try_catch_sync!(self, cached_frame, self.compare_ord(Ordering::is_gt)),\n                Opcode::CompareGe => try_catch_sync!(self, cached_frame, self.compare_ord(Ordering::is_ge)),\n                Opcode::CompareIs => self.compare_is(false),\n                Opcode::CompareIsNot => self.compare_is(true),\n                Opcode::CompareIn => try_catch_sync!(self, cached_frame, self.compare_in(false)),\n                Opcode::CompareNotIn => try_catch_sync!(self, cached_frame, self.compare_in(true)),\n                Opcode::CompareModEq => {\n                    let const_idx = fetch_u16!(cached_frame);\n                    let k = cached_frame.code.constants().get(const_idx);\n                    try_catch_sync!(self, cached_frame, self.compare_mod_eq(k));\n                }\n                // Unary Operations\n                Opcode::UnaryNot => {\n                    let value = self.pop();\n                    let result = !value.py_bool(self);\n                    value.drop_with_heap(self);\n                    self.push(Value::Bool(result));\n                }\n                Opcode::UnaryNeg => {\n                    // Unary minus - negate numeric value\n                    let value = self.pop();\n                    match value {\n                        Value::Int(n) => {\n                            // Use checked_neg to handle i64::MIN overflow\n                            if let Some(negated) = n.checked_neg() {\n                                self.push(Value::Int(negated));\n                            } else {\n                                // i64::MIN negated overflows to LongInt\n                                let li = -LongInt::from(n);\n                                match li.into_value(self.heap) {\n                                    Ok(v) => self.push(v),\n                                    Err(e) => catch_sync!(self, cached_frame, RunError::from(e)),\n                                }\n                            }\n                        }\n                        Value::Float(f) => self.push(Value::Float(-f)),\n                        Value::Bool(b) => self.push(Value::Int(if b { -1 } else { 0 })),\n                        Value::Ref(id) => {\n                            if let HeapData::LongInt(li) = self.heap.get(id) {\n                                let negated = -LongInt::new(li.inner().clone());\n                                value.drop_with_heap(self);\n                                match negated.into_value(self.heap) {\n                                    Ok(v) => self.push(v),\n                                    Err(e) => catch_sync!(self, cached_frame, RunError::from(e)),\n                                }\n                            } else {\n                                let value_type = value.py_type(self.heap);\n                                value.drop_with_heap(self);\n                                catch_sync!(self, cached_frame, ExcType::unary_type_error(\"-\", value_type));\n                            }\n                        }\n                        _ => {\n                            let value_type = value.py_type(self.heap);\n                            value.drop_with_heap(self);\n                            catch_sync!(self, cached_frame, ExcType::unary_type_error(\"-\", value_type));\n                        }\n                    }\n                }\n                Opcode::UnaryPos => {\n                    // Unary plus - converts bools to int, no-op for other numbers\n                    let value = self.pop();\n                    match value {\n                        Value::Int(_) | Value::Float(_) => self.push(value),\n                        Value::Bool(b) => self.push(Value::Int(i64::from(b))),\n                        Value::Ref(id) => {\n                            if matches!(self.heap.get(id), HeapData::LongInt(_)) {\n                                // LongInt - return as-is (value already has correct refcount)\n                                self.push(value);\n                            } else {\n                                let value_type = value.py_type(self.heap);\n                                value.drop_with_heap(self);\n                                catch_sync!(self, cached_frame, ExcType::unary_type_error(\"+\", value_type));\n                            }\n                        }\n                        _ => {\n                            let value_type = value.py_type(self.heap);\n                            value.drop_with_heap(self);\n                            catch_sync!(self, cached_frame, ExcType::unary_type_error(\"+\", value_type));\n                        }\n                    }\n                }\n                Opcode::UnaryInvert => {\n                    // Bitwise NOT\n                    let value = self.pop();\n                    match value {\n                        Value::Int(n) => self.push(Value::Int(!n)),\n                        Value::Bool(b) => self.push(Value::Int(!i64::from(b))),\n                        Value::Ref(id) => {\n                            if let HeapData::LongInt(li) = self.heap.get(id) {\n                                // LongInt bitwise NOT: ~x = -(x + 1)\n                                let inverted = -(li.inner() + 1i32);\n                                value.drop_with_heap(self);\n                                match LongInt::new(inverted).into_value(self.heap) {\n                                    Ok(v) => self.push(v),\n                                    Err(e) => catch_sync!(self, cached_frame, RunError::from(e)),\n                                }\n                            } else {\n                                let value_type = value.py_type(self.heap);\n                                value.drop_with_heap(self);\n                                catch_sync!(self, cached_frame, ExcType::unary_type_error(\"~\", value_type));\n                            }\n                        }\n                        _ => {\n                            let value_type = value.py_type(self.heap);\n                            value.drop_with_heap(self);\n                            catch_sync!(self, cached_frame, ExcType::unary_type_error(\"~\", value_type));\n                        }\n                    }\n                }\n                // In-place Operations - route through exception handling\n                Opcode::InplaceAdd => try_catch_sync!(self, cached_frame, self.inplace_add()),\n                // Other in-place ops use the same logic as binary ops for now\n                Opcode::InplaceSub => try_catch_sync!(self, cached_frame, self.binary_sub()),\n                Opcode::InplaceMul => try_catch_sync!(self, cached_frame, self.binary_mult()),\n                Opcode::InplaceDiv => try_catch_sync!(self, cached_frame, self.binary_div()),\n                Opcode::InplaceFloorDiv => try_catch_sync!(self, cached_frame, self.binary_floordiv()),\n                Opcode::InplaceMod => try_catch_sync!(self, cached_frame, self.binary_mod()),\n                Opcode::InplacePow => try_catch_sync!(self, cached_frame, self.binary_pow()),\n                Opcode::InplaceAnd => {\n                    try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::And));\n                }\n                Opcode::InplaceOr => try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::Or)),\n                Opcode::InplaceXor => {\n                    try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::Xor));\n                }\n                Opcode::InplaceLShift => {\n                    try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::LShift));\n                }\n                Opcode::InplaceRShift => {\n                    try_catch_sync!(self, cached_frame, self.binary_bitwise(BitwiseOp::RShift));\n                }\n                // Collection Building - route through exception handling\n                Opcode::BuildList => {\n                    let count = fetch_u16!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.build_list(count));\n                }\n                Opcode::BuildTuple => {\n                    let count = fetch_u16!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.build_tuple(count));\n                }\n                Opcode::BuildDict => {\n                    let count = fetch_u16!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.build_dict(count));\n                }\n                Opcode::BuildSet => {\n                    let count = fetch_u16!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.build_set(count));\n                }\n                Opcode::FormatValue => {\n                    let flags = fetch_u8!(cached_frame);\n                    try_catch_sync!(self, cached_frame, self.format_value(flags));\n                }\n                Opcode::BuildFString => {\n                    let count = fetch_u16!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.build_fstring(count));\n                }\n                Opcode::BuildSlice => {\n                    try_catch_sync!(self, cached_frame, self.build_slice());\n                }\n                Opcode::ListExtend => {\n                    try_catch_sync!(self, cached_frame, self.list_extend());\n                }\n                Opcode::ListToTuple => {\n                    try_catch_sync!(self, cached_frame, self.list_to_tuple());\n                }\n                Opcode::DictMerge => {\n                    let func_name_id = fetch_u16!(cached_frame);\n                    try_catch_sync!(self, cached_frame, self.dict_merge(func_name_id));\n                }\n                // PEP 448 literal building\n                Opcode::DictUpdate => {\n                    let depth = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.dict_update(depth));\n                }\n                Opcode::SetExtend => {\n                    let depth = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.set_extend(depth));\n                }\n                // Comprehension Building - append/add/set items during iteration\n                Opcode::ListAppend => {\n                    let depth = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.list_append(depth));\n                }\n                Opcode::SetAdd => {\n                    let depth = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.set_add(depth));\n                }\n                Opcode::DictSetItem => {\n                    let depth = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.dict_set_item(depth));\n                }\n                // Subscript & Attribute - route through exception handling\n                Opcode::BinarySubscr => {\n                    let index = self.pop();\n                    let obj = self.pop();\n                    let result = obj.py_getitem(&index, self);\n                    obj.drop_with_heap(self);\n                    index.drop_with_heap(self);\n                    match result {\n                        Ok(v) => self.push(v),\n                        Err(e) => catch_sync!(self, cached_frame, e),\n                    }\n                }\n                Opcode::StoreSubscr => {\n                    // Stack order: value, obj, index (TOS)\n                    let index = self.pop();\n                    let mut obj = self.pop();\n                    let value = self.pop();\n                    let result = obj.py_setitem(index, value, self);\n                    obj.drop_with_heap(self);\n                    if let Err(e) = result {\n                        catch_sync!(self, cached_frame, e);\n                    }\n                }\n                Opcode::LoadAttr => {\n                    let name_idx = fetch_u16!(cached_frame);\n                    let name_id = StringId::from_index(name_idx);\n                    handle_call_result!(self, cached_frame, self.load_attr(name_id));\n                }\n                Opcode::LoadAttrImport => {\n                    let name_idx = fetch_u16!(cached_frame);\n                    let name_id = StringId::from_index(name_idx);\n                    handle_call_result!(self, cached_frame, self.load_attr_import(name_id));\n                }\n                Opcode::StoreAttr => {\n                    let name_idx = fetch_u16!(cached_frame);\n                    let name_id = StringId::from_index(name_idx);\n                    try_catch_sync!(self, cached_frame, self.store_attr(name_id));\n                }\n                // Control Flow - use cached_frame.ip directly for jumps\n                Opcode::Jump => {\n                    let offset = fetch_i16!(cached_frame);\n                    jump_relative!(cached_frame.ip, offset);\n                }\n                Opcode::JumpIfTrue => {\n                    let offset = fetch_i16!(cached_frame);\n                    let cond = self.pop();\n                    if cond.py_bool(self) {\n                        jump_relative!(cached_frame.ip, offset);\n                    }\n                    cond.drop_with_heap(self);\n                }\n                Opcode::JumpIfFalse => {\n                    let offset = fetch_i16!(cached_frame);\n                    let cond = self.pop();\n                    if !cond.py_bool(self) {\n                        jump_relative!(cached_frame.ip, offset);\n                    }\n                    cond.drop_with_heap(self);\n                }\n                Opcode::JumpIfTrueOrPop => {\n                    let offset = fetch_i16!(cached_frame);\n                    if self.peek().py_bool(self) {\n                        jump_relative!(cached_frame.ip, offset);\n                    } else {\n                        let value = self.pop();\n                        value.drop_with_heap(self);\n                    }\n                }\n                Opcode::JumpIfFalseOrPop => {\n                    let offset = fetch_i16!(cached_frame);\n                    if self.peek().py_bool(self) {\n                        let value = self.pop();\n                        value.drop_with_heap(self);\n                    } else {\n                        jump_relative!(cached_frame.ip, offset);\n                    }\n                }\n                // Iteration - route through exception handling\n                Opcode::GetIter => {\n                    let value = self.pop();\n                    // Create a MontyIter from the value and store on heap\n                    match MontyIter::new(value, self) {\n                        Ok(iter) => match self.heap.allocate(HeapData::Iter(iter)) {\n                            Ok(heap_id) => self.push(Value::Ref(heap_id)),\n                            Err(e) => catch_sync!(self, cached_frame, e.into()),\n                        },\n                        Err(e) => catch_sync!(self, cached_frame, e),\n                    }\n                }\n                Opcode::ForIter => {\n                    let offset = fetch_i16!(cached_frame);\n                    // Peek at the iterator on TOS and extract heap_id\n                    let Value::Ref(heap_id) = *self.peek() else {\n                        return Err(RunError::internal(\"ForIter: expected iterator ref on stack\"));\n                    };\n\n                    // Use advance_iterator which avoids std::mem::replace overhead\n                    // by using a two-phase approach: read state, get value, update index\n                    match advance_on_heap(self.heap, heap_id, self.interns) {\n                        Ok(Some(value)) => self.push(value),\n                        Ok(None) => {\n                            // Iterator exhausted - pop it and jump to end\n                            let iter = self.pop();\n                            iter.drop_with_heap(self);\n                            jump_relative!(cached_frame.ip, offset);\n                        }\n                        Err(e) => {\n                            // Error during iteration (e.g., dict size changed)\n                            let iter = self.pop();\n                            iter.drop_with_heap(self);\n                            catch_sync!(self, cached_frame, e);\n                        }\n                    }\n                }\n                // Function Calls - sync IP before call, reload cache after frame changes\n                Opcode::CallFunction => {\n                    let arg_count = fetch_u8!(cached_frame) as usize;\n\n                    // Sync IP before call (call_function may access frame for traceback)\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    handle_call_result!(self, cached_frame, self.exec_call_function(arg_count));\n                }\n                Opcode::CallBuiltinFunction => {\n                    // Fetch operands: builtin_id (u8) + arg_count (u8)\n                    let builtin_id = fetch_u8!(cached_frame);\n                    let arg_count = fetch_u8!(cached_frame) as usize;\n\n                    // Sync IP before call (builtins like map() may call evaluate_function\n                    // which pushes frames and runs a nested run() loop)\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    match self.exec_call_builtin_function(builtin_id, arg_count) {\n                        Ok(result) => self.push(result),\n                        Err(err) => catch_sync!(self, cached_frame, err),\n                    }\n                }\n                Opcode::CallBuiltinType => {\n                    // Fetch operands: type_id (u8) + arg_count (u8)\n                    let type_id = fetch_u8!(cached_frame);\n                    let arg_count = fetch_u8!(cached_frame) as usize;\n\n                    match self.exec_call_builtin_type(type_id, arg_count) {\n                        Ok(result) => self.push(result),\n                        // IP sync deferred to error path (no frame push possible)\n                        Err(err) => catch_sync!(self, cached_frame, err),\n                    }\n                }\n                Opcode::CallFunctionKw => {\n                    // Fetch operands: pos_count, kw_count, then kw_count name indices\n                    let pos_count = fetch_u8!(cached_frame) as usize;\n                    let kw_count = fetch_u8!(cached_frame) as usize;\n\n                    // Read keyword name StringIds\n                    let mut kwname_ids = Vec::with_capacity(kw_count);\n                    for _ in 0..kw_count {\n                        kwname_ids.push(StringId::from_index(fetch_u16!(cached_frame)));\n                    }\n\n                    // Sync IP before call (call_function may access frame for traceback)\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    handle_call_result!(self, cached_frame, self.exec_call_function_kw(pos_count, kwname_ids));\n                }\n                Opcode::CallAttr => {\n                    // CallAttr: u16 name_id, u8 arg_count\n                    // Stack: [obj, arg1, arg2, ..., argN] -> [result]\n                    let name_idx = fetch_u16!(cached_frame);\n                    let arg_count = fetch_u8!(cached_frame) as usize;\n                    let name_id = StringId::from_index(name_idx);\n\n                    // Sync IP before call (may yield to host for OS/external calls)\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    handle_call_result!(self, cached_frame, self.exec_call_attr(name_id, arg_count));\n                }\n                Opcode::CallAttrKw => {\n                    // CallAttrKw: u16 name_id, u8 pos_count, u8 kw_count, then kw_count u16 name indices\n                    // Stack: [obj, pos_args..., kw_values...] -> [result]\n                    let name_idx = fetch_u16!(cached_frame);\n                    let pos_count = fetch_u8!(cached_frame) as usize;\n                    let kw_count = fetch_u8!(cached_frame) as usize;\n                    let name_id = StringId::from_index(name_idx);\n\n                    // Read keyword name StringIds\n                    let mut kwname_ids = Vec::with_capacity(kw_count);\n                    for _ in 0..kw_count {\n                        kwname_ids.push(StringId::from_index(fetch_u16!(cached_frame)));\n                    }\n\n                    // Sync IP before call (may yield to host for OS/external calls)\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    handle_call_result!(\n                        self,\n                        cached_frame,\n                        self.exec_call_attr_kw(name_id, pos_count, kwname_ids)\n                    );\n                }\n                Opcode::CallFunctionExtended => {\n                    let flags = fetch_u8!(cached_frame);\n                    let has_kwargs = (flags & 0x01) != 0;\n\n                    // Sync IP before call\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    handle_call_result!(self, cached_frame, self.exec_call_function_extended(has_kwargs));\n                }\n                Opcode::CallAttrExtended => {\n                    let name_idx = fetch_u16!(cached_frame);\n                    let flags = fetch_u8!(cached_frame);\n                    let name_id = StringId::from_index(name_idx);\n                    let has_kwargs = (flags & 0x01) != 0;\n\n                    // Sync IP before call (may yield to host for OS/external calls)\n                    self.current_frame_mut().ip = cached_frame.ip;\n\n                    handle_call_result!(self, cached_frame, self.exec_call_attr_extended(name_id, has_kwargs));\n                }\n                // Function Definition\n                Opcode::MakeFunction => {\n                    let func_idx = fetch_u16!(cached_frame);\n                    let defaults_count = fetch_u8!(cached_frame) as usize;\n                    let func_id = FunctionId::from_index(func_idx);\n\n                    if defaults_count == 0 {\n                        // No defaults - use inline Value::Function (no heap allocation)\n                        self.push(Value::DefFunction(func_id));\n                    } else {\n                        // Pop default values from stack (drain maintains order: first pushed = first in vec)\n                        let defaults = self.pop_n(defaults_count);\n\n                        // Create FunctionDefaults on heap and push reference\n                        let heap_id = self\n                            .heap\n                            .allocate(HeapData::FunctionDefaults(FunctionDefaults { func_id, defaults }))?;\n                        self.push(Value::Ref(heap_id));\n                    }\n                }\n                Opcode::MakeClosure => {\n                    let func_idx = fetch_u16!(cached_frame);\n                    let defaults_count = fetch_u8!(cached_frame) as usize;\n                    let cell_count = fetch_u8!(cached_frame) as usize;\n                    let func_id = FunctionId::from_index(func_idx);\n\n                    // Pop cells from stack (pushed after defaults, so on top)\n                    // Cells are Value::Ref pointing to HeapData::Cell\n                    // We use individual pops which reverses order, so we need to reverse back\n                    let mut cells = Vec::with_capacity(cell_count);\n                    for _ in 0..cell_count {\n                        // mut needed for dec_ref_forget when ref-count-panic feature is enabled\n                        #[cfg_attr(not(feature = \"ref-count-panic\"), expect(unused_mut))]\n                        let mut cell_val = self.pop();\n                        match &cell_val {\n                            Value::Ref(heap_id) => {\n                                // Keep the reference - the Closure will own the HeapId\n                                cells.push(*heap_id);\n                                // Mark the Value as dereferenced since Closure takes ownership\n                                // of the reference count (we don't call drop_with_heap because\n                                // we're not decrementing the refcount, just transferring it)\n                                #[cfg(feature = \"ref-count-panic\")]\n                                cell_val.dec_ref_forget();\n                            }\n                            _ => {\n                                return Err(RunError::internal(\"MakeClosure: expected cell reference on stack\"));\n                            }\n                        }\n                    }\n                    // Reverse to get original order (individual pops reverse the order)\n                    cells.reverse();\n\n                    // Pop default values from stack (drain maintains order: first pushed = first in vec)\n                    let defaults = self.pop_n(defaults_count);\n\n                    // Create Closure on heap and push reference\n                    let heap_id = self.heap.allocate(HeapData::Closure(Closure {\n                        func_id,\n                        cells,\n                        defaults,\n                    }))?;\n                    self.push(Value::Ref(heap_id));\n                }\n                // Exception Handling\n                Opcode::Raise => {\n                    let exc = self.pop();\n                    let error = self.make_exception(exc, true); // is_raise=true, hide caret\n                    catch_sync!(self, cached_frame, error);\n                }\n                Opcode::Reraise => {\n                    // Pop the current exception from the stack to re-raise it\n                    // If caught, handle_exception will push it back\n                    let error = if let Some(exc) = self.exception_stack.pop() {\n                        self.make_exception(exc, true) // is_raise=true for reraise\n                    } else {\n                        // No active exception - create a RuntimeError\n                        SimpleException::new_msg(ExcType::RuntimeError, \"No active exception to reraise\").into()\n                    };\n                    catch_sync!(self, cached_frame, error);\n                }\n                Opcode::ClearException => {\n                    // Pop the current exception from the stack\n                    // This restores the previous exception context (if any)\n                    if let Some(exc) = self.exception_stack.pop() {\n                        exc.drop_with_heap(self);\n                    }\n                }\n                Opcode::CheckExcMatch => {\n                    // Stack: [exception, exc_type] -> [exception, bool]\n                    let exc_type = self.pop();\n                    let exception = self.peek();\n                    let result = self.check_exc_match(exception, &exc_type);\n                    exc_type.drop_with_heap(self);\n                    let result = result?;\n                    self.push(Value::Bool(result));\n                }\n                // Return - reload cache after popping frame\n                Opcode::ReturnValue => {\n                    let value = self.pop();\n                    if self.frames.len() == 1 {\n                        // Last frame - check if this is main task or spawned task\n                        let is_main_task = self.is_main_task();\n\n                        if is_main_task {\n                            // Module-level return - we're done\n                            return Ok(FrameExit::Return(value));\n                        }\n\n                        // Spawned task completed - handle task completion\n                        let result = self.handle_task_completion(value);\n                        match result {\n                            Ok(AwaitResult::ValueReady(v)) => {\n                                self.push(v);\n                            }\n                            Ok(AwaitResult::FramePushed) => {\n                                // Switched to another task - reload cache\n                                reload_cache!(self, cached_frame);\n                            }\n                            Ok(AwaitResult::Yield(pending)) => {\n                                // All tasks blocked - return to host\n                                return Ok(FrameExit::ResolveFutures(pending));\n                            }\n                            Err(e) => {\n                                catch_sync!(self, cached_frame, e);\n                            }\n                        }\n                        continue;\n                    }\n                    // Pop current frame and push return value\n                    if self.pop_frame() {\n                        // This frame indicated evaluation should stop - return to host with value\n                        // e.g. `evaluate_function`\n                        return Ok(FrameExit::Return(value));\n                    }\n                    self.push(value);\n                    // Reload cache from parent frame\n                    reload_cache!(self, cached_frame);\n                }\n                // Async/Await\n                Opcode::Await => {\n                    // Sync IP before exec (may push new frame for coroutine)\n                    self.current_frame_mut().ip = cached_frame.ip;\n                    let result = self.exec_get_awaitable();\n                    match result {\n                        Ok(AwaitResult::ValueReady(value)) => {\n                            self.push(value);\n                        }\n                        Ok(AwaitResult::FramePushed) => {\n                            // Reload cache after pushing a new frame\n                            reload_cache!(self, cached_frame);\n                        }\n                        Ok(AwaitResult::Yield(pending_calls)) => {\n                            // All tasks are blocked - return control to host\n                            return Ok(FrameExit::ResolveFutures(pending_calls));\n                        }\n                        Err(e) => {\n                            catch_sync!(self, cached_frame, e);\n                        }\n                    }\n                }\n                // Unpacking - route through exception handling\n                Opcode::UnpackSequence => {\n                    let count = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.unpack_sequence(count));\n                }\n                Opcode::UnpackEx => {\n                    let before = fetch_u8!(cached_frame) as usize;\n                    let after = fetch_u8!(cached_frame) as usize;\n                    try_catch_sync!(self, cached_frame, self.unpack_ex(before, after));\n                }\n                // Special\n                Opcode::Nop => {\n                    // No operation\n                }\n                // Module Operations\n                Opcode::LoadModule => {\n                    let module_id = fetch_u8!(cached_frame);\n                    try_catch_sync!(self, cached_frame, self.load_module(module_id));\n                }\n                Opcode::RaiseImportError => {\n                    // Fetch the module name from the constant pool and raise ModuleNotFoundError\n                    let const_idx = fetch_u16!(cached_frame);\n                    let module_name = cached_frame.code.constants().get(const_idx);\n                    // The constant should be an InternString from compile_import/compile_import_from\n                    let name_str = match module_name {\n                        Value::InternString(id) => self.interns.get_str(*id),\n                        _ => \"<unknown>\",\n                    };\n                    let error = ExcType::module_not_found_error(name_str);\n                    catch_sync!(self, cached_frame, error);\n                }\n            }\n        }\n    }\n\n    /// Loads a built-in module and pushes it onto the stack.\n    fn load_module(&mut self, module_id: u8) -> RunResult<()> {\n        let module = BuiltinModule::from_repr(module_id).expect(\"unknown module id\");\n\n        // Create the module on the heap using pre-interned strings\n        let heap_id = module.create(self)?;\n        self.push(Value::Ref(heap_id));\n        Ok(())\n    }\n\n    /// Resumes execution after an external call completes.\n    ///\n    /// Pushes the return value onto the stack and continues execution.\n    pub fn resume(&mut self, obj: MontyObject) -> Result<FrameExit, RunError> {\n        let value = obj\n            .to_value(self)\n            .map_err(|e| SimpleException::new(ExcType::RuntimeError, Some(format!(\"invalid return type: {e}\"))))?;\n        self.push(value);\n        self.run()\n    }\n\n    /// Sets the instruction IP used for exception table lookup and traceback generation.\n    ///\n    /// Used by `run()` to restore the IP to the load instruction's position before\n    /// raising `NameError` for auto-injected `ExtFunction` values, so the traceback\n    /// points to the name reference rather than the call expression.\n    pub fn set_instruction_ip(&mut self, ip: usize) {\n        self.instruction_ip = ip;\n    }\n\n    /// Resumes execution after an external call raised an exception.\n    ///\n    /// Uses the exception handling mechanism to try to catch the exception.\n    /// If caught, continues execution at the handler. If not, propagates the error.\n    pub fn resume_with_exception(&mut self, error: RunError) -> Result<FrameExit, RunError> {\n        // Use the normal exception handling mechanism\n        // handle_exception returns None if caught, Some(error) if not caught\n        if let Some(uncaught_error) = self.handle_exception(error) {\n            return Err(uncaught_error);\n        }\n        // Exception was caught, continue execution\n        self.run()\n    }\n\n    // ========================================================================\n    // Stack Operations\n    // ========================================================================\n\n    /// Pushes a value onto the operand stack.\n    #[inline]\n    pub(crate) fn push(&mut self, value: Value) {\n        self.stack.push(value);\n    }\n\n    /// Pops a value from the operand stack.\n    #[inline]\n    pub(super) fn pop(&mut self) -> Value {\n        self.stack.pop().expect(\"stack underflow\")\n    }\n\n    /// Peeks at the top of the operand stack without removing it.\n    #[inline]\n    pub(super) fn peek(&self) -> &Value {\n        self.stack.last().expect(\"stack underflow\")\n    }\n\n    /// Pops n values from the stack in reverse order (first popped is last in vec).\n    pub(super) fn pop_n(&mut self, n: usize) -> Vec<Value> {\n        let start = self.stack.len() - n;\n        self.stack.drain(start..).collect()\n    }\n\n    // ========================================================================\n    // Frame Operations\n    // ========================================================================\n\n    /// Returns a reference to the current (topmost) call frame.\n    #[inline]\n    pub(crate) fn current_frame(&self) -> &CallFrame<'a> {\n        self.frames.last().expect(\"no active frame\")\n    }\n\n    /// Creates a new cached frame from the current frame.\n    #[inline]\n    pub(super) fn new_cached_frame(&self) -> CachedFrame<'a> {\n        self.current_frame().into()\n    }\n\n    /// Returns a mutable reference to the current call frame.\n    #[inline]\n    pub(super) fn current_frame_mut(&mut self) -> &mut CallFrame<'a> {\n        self.frames.last_mut().expect(\"no active frame\")\n    }\n\n    /// Pushes the given frame onto the call stack.\n    ///\n    /// Returns an error if the recursion depth limit is exceeded by pushing this frame.\n    pub(super) fn push_frame(&mut self, frame: CallFrame<'a>) -> RunResult<()> {\n        // root frame doesn't count towards recursion depth, so only check if there's already a frame on the stack\n        if !self.frames.is_empty()\n            && let Err(e) = self.heap.incr_recursion_depth()\n        {\n            self.cleanup_frame_state(&frame);\n            return Err(e.into());\n        }\n        self.frames.push(frame);\n\n        Ok(())\n    }\n\n    /// Pops the current frame from the call stack.\n    ///\n    /// Cleans up the frame's stack region and namespace (except for global namespace).\n    /// Syncs `instruction_ip` to the parent frame's IP so that exception handling\n    /// looks up handlers in the correct frame's exception table.\n    ///\n    /// Returns `true` if this frame indicated evaluation should stop when popped.\n    pub(super) fn pop_frame(&mut self) -> bool {\n        let frame = self.frames.pop().expect(\"no frame to pop\");\n        self.cleanup_frame_state(&frame);\n        // Sync instruction_ip to the parent frame so exception table lookups\n        // target the correct frame after returning from a nested run() call.\n        if let Some(parent) = self.frames.last() {\n            self.instruction_ip = parent.ip;\n        }\n        // Decrement recursion depth if this wasn't the root frame\n        if !self.frames.is_empty() {\n            self.heap.decr_recursion_depth();\n        }\n        frame.should_return\n    }\n\n    fn cleanup_frame_state(&mut self, frame: &CallFrame<'_>) {\n        // Clean up frame's stack region (locals + operands).\n        // Locals occupy stack[frame.stack_base..frame.stack_base + frame.locals_count],\n        // operands are above that. Draining from stack_base covers both.\n        self.stack\n            .drain(frame.stack_base..)\n            .for_each(|value| value.drop_with_heap(&mut *self.heap));\n\n        // Track freed memory for locals\n        if frame.locals_count > 0 {\n            let size = frame.locals_count as usize * std::mem::size_of::<Value>();\n            self.heap.tracker_mut().on_free(|| size);\n        }\n    }\n\n    /// Cleans up all frames and stack values for the current task.\n    ///\n    /// Used when a task completes or fails and we need to switch to another task.\n    /// Drains the stack with proper `drop_with_heap` for each value (since locals\n    /// are inlined on the stack), then cleans up each frame's cell references.\n    pub(super) fn cleanup_current_task(&mut self) {\n        self.stack.drain(..).drop_with_heap(self.heap);\n        self.frames.clear();\n    }\n\n    /// Runs garbage collection with proper GC roots.\n    ///\n    /// GC roots include values in the stack (locals + operands), globals, and exception stack.\n    fn run_gc(&mut self) {\n        // Collect roots from all reachable values\n        let stack_roots = self.stack.iter().filter_map(Value::ref_id);\n        let globals_roots = self.globals.iter().filter_map(Value::ref_id);\n        let exc_roots = self.exception_stack.iter().filter_map(Value::ref_id);\n\n        // Collect all roots into a vec to avoid lifetime issues\n        let roots: Vec<HeapId> = stack_roots.chain(globals_roots).chain(exc_roots).collect();\n\n        self.heap.collect_garbage(roots);\n    }\n\n    /// Returns the current source position for traceback generation.\n    ///\n    /// Uses `instruction_ip` which is set at the start of each instruction in the run loop,\n    /// ensuring accurate position tracking even when using cached IP for bytecode fetching.\n    pub(super) fn current_position(&self) -> CodeRange {\n        let frame = self.current_frame();\n        // Use instruction_ip which points to the start of the current instruction\n        // (set at the beginning of each loop iteration in run())\n        frame\n            .code\n            .location_for_offset(self.instruction_ip)\n            .map(crate::bytecode::code::LocationEntry::range)\n            .unwrap_or_default()\n    }\n\n    // ========================================================================\n    // Variable Operations\n    // ========================================================================\n\n    /// Loads a local variable and pushes it onto the stack.\n    ///\n    /// For true locals (assigned somewhere in the function), returns `UnboundLocalError`\n    /// if accessed before assignment. For unassigned names (never assigned in this scope),\n    /// returns `NameLookup` to signal that the host should resolve the name.\n    ///\n    /// Returns `Ok(None)` for normal loads, `Ok(Some(FrameExit::NameLookup))` when\n    /// the host needs to resolve an unknown name, or `Err` for true unbound locals.\n    fn load_local(&mut self, cached_frame: &CachedFrame<'a>, slot: u16) -> Result<Option<FrameExit>, RunError> {\n        let value = &self.stack[cached_frame.stack_base + slot as usize];\n\n        // Check for undefined value — raise appropriate error based on whether\n        // this is a true local (assigned somewhere) or an undefined reference\n        if matches!(value, Value::Undefined) {\n            let name = cached_frame.code.local_name(slot);\n            if cached_frame.code.is_assigned_local(slot) {\n                // True local accessed before assignment\n                return Err(self.unbound_local_error(slot, name));\n            }\n            // Name doesn't exist in any scope — yield to host for resolution.\n            let name_id = name.expect(\"LocalUnassigned should always have a name\");\n            return Ok(Some(FrameExit::NameLookup {\n                name_id,\n                namespace_slot: slot,\n                is_global: false,\n            }));\n        }\n\n        self.push(value.clone_with_heap(self.heap));\n        Ok(None)\n    }\n\n    /// Loads a local variable in call context, pushing `ExtFunction` for undefined names.\n    ///\n    /// Unlike `load_local`, this never yields `NameLookup`. When the variable is undefined\n    /// (a `LocalUnassigned` name), it pushes `Value::ExtFunction(name_id)` so that the\n    /// subsequent `CallFunction` opcode can yield `FunctionCall` instead.\n    fn load_local_callable(&mut self, cached_frame: &CachedFrame<'a>, slot: u16, name_id: StringId) {\n        let value = &self.stack[cached_frame.stack_base + slot as usize];\n\n        if matches!(value, Value::Undefined) {\n            // LocalUnassigned in call context — push ExtFunction for the host to handle.\n            self.ext_function_load_ip = Some(self.instruction_ip);\n            self.push(Value::ExtFunction(name_id));\n        } else {\n            self.push(value.clone_with_heap(self.heap));\n        }\n    }\n\n    /// Loads a global variable in call context, pushing `ExtFunction` for undefined names.\n    ///\n    /// Unlike `load_global`, this never yields `NameLookup`. When the variable is undefined,\n    /// it pushes `Value::ExtFunction(name_id)` so that the subsequent `CallFunction` opcode\n    /// can yield `FunctionCall` instead.\n    fn load_global_callable(&mut self, slot: u16, name_id: StringId) {\n        let value = self.globals[slot as usize].clone_with_heap(self.heap);\n\n        if matches!(value, Value::Undefined) {\n            // Save the load instruction's IP so NameError tracebacks point to the name\n            self.ext_function_load_ip = Some(self.instruction_ip);\n            self.push(Value::ExtFunction(name_id));\n        } else {\n            self.push(value);\n        }\n    }\n\n    /// Creates an UnboundLocalError for a local variable accessed before assignment.\n    fn unbound_local_error(&self, slot: u16, name: Option<StringId>) -> RunError {\n        let name_str = match name {\n            Some(id) => self.interns.get_str(id).to_string(),\n            None => format!(\"<local {slot}>\"),\n        };\n        ExcType::unbound_local_error(&name_str).into()\n    }\n\n    /// Creates a NameError for an undefined global variable.\n    fn name_error(&self, slot: u16, name: Option<StringId>) -> RunError {\n        let name_str = match name {\n            Some(id) => self.interns.get_str(id).to_string(),\n            None => format!(\"<global {slot}>\"),\n        };\n        ExcType::name_error(&name_str).into()\n    }\n\n    /// Pops the top of stack and stores it in a local variable.\n    fn store_local(&mut self, cached_frame: &CachedFrame<'a>, slot: u16) {\n        let value = self.pop();\n        let target = &mut self.stack[cached_frame.stack_base + slot as usize];\n        let old_value = std::mem::replace(target, value);\n        old_value.drop_with_heap(self);\n    }\n\n    /// Deletes a local variable (sets it to Undefined).\n    fn delete_local(&mut self, cached_frame: &CachedFrame<'a>, slot: u16) {\n        let target = &mut self.stack[cached_frame.stack_base + slot as usize];\n        let old_value = std::mem::replace(target, Value::Undefined);\n        old_value.drop_with_heap(self);\n    }\n\n    /// Loads a global variable and pushes it onto the stack.\n    ///\n    /// When the variable is undefined, yields `NameLookup` to the host for resolution\n    /// instead of immediately raising `NameError`. This allows the host to provide\n    /// external function bindings lazily.\n    fn load_global(&mut self, slot: u16) -> Result<Option<FrameExit>, RunError> {\n        let value = self.globals[slot as usize].clone_with_heap(self.heap);\n\n        // Check for undefined value — raise appropriate error or yield to host\n        if matches!(value, Value::Undefined) {\n            let name = self.current_frame().code.local_name(slot);\n\n            // If the name is registered as an assigned local (e.g. a module-level\n            // variable or comprehension loop variable), raise UnboundLocalError\n            // immediately rather than yielding NameLookup.\n            if self.current_frame().code.is_assigned_local(slot) {\n                return Err(self.unbound_local_error(slot, name));\n            }\n\n            let Some(name_id) = name else {\n                // No name available — raise NameError directly\n                return Err(self.name_error(slot, None));\n            };\n            Ok(Some(FrameExit::NameLookup {\n                name_id,\n                namespace_slot: slot,\n                is_global: true,\n            }))\n        } else {\n            self.push(value);\n            Ok(None)\n        }\n    }\n\n    /// Pops the top of stack and stores it in a global variable.\n    fn store_global(&mut self, slot: u16) {\n        let value = self.pop();\n        let old_value = std::mem::replace(&mut self.globals[slot as usize], value);\n        old_value.drop_with_heap(self);\n    }\n\n    /// Deletes a global variable (sets it to Undefined).\n    fn delete_global(&mut self, slot: u16) {\n        let old_value = std::mem::replace(&mut self.globals[slot as usize], Value::Undefined);\n        old_value.drop_with_heap(self);\n    }\n\n    /// Loads from a closure cell and pushes onto the stack.\n    ///\n    /// The cell `HeapId` is read from the frame's local variable slot on the stack\n    /// (cells are stored as `Value::Ref(cell_id)` at known positions in the locals region).\n    /// Returns a `NameError` if the cell value is undefined (free variable not bound).\n    fn load_cell(&mut self, cached_frame: &CachedFrame<'a>, slot: u16) -> RunResult<()> {\n        let cell_id = self.cell_id_from_local(cached_frame, slot);\n        let value = match self.heap.get(cell_id) {\n            HeapData::Cell(c) => c.0.clone_with_heap(self),\n            _ => panic!(\"LoadCell: entry is not a Cell\"),\n        };\n\n        // Check for undefined value - raise NameError for unbound free variable\n        if matches!(value, Value::Undefined) {\n            value.drop_with_heap(self);\n            let name = cached_frame.code.local_name(slot);\n            return Err(self.free_var_error(name));\n        }\n\n        self.push(value);\n        Ok(())\n    }\n\n    /// Extracts the cell `HeapId` from a local variable slot on the stack.\n    ///\n    /// Cell variables are stored as `Value::Ref(cell_id)` in the frame's locals region.\n    fn cell_id_from_local(&self, cached_frame: &CachedFrame<'_>, slot: u16) -> HeapId {\n        match &self.stack[cached_frame.stack_base + slot as usize] {\n            Value::Ref(cell_id) => *cell_id,\n            other => panic!(\"LoadCell/StoreCell: expected cell reference in local slot {slot}, found {other:?}\"),\n        }\n    }\n\n    /// Creates a NameError for an unbound free variable.\n    fn free_var_error(&self, name: Option<StringId>) -> RunError {\n        let name_str = match name {\n            Some(id) => self.interns.get_str(id).to_string(),\n            None => \"<free var>\".to_string(),\n        };\n        ExcType::name_error_free_variable(&name_str).into()\n    }\n\n    /// Pops the top of stack and stores it in a closure cell.\n    ///\n    /// The cell `HeapId` is read from the frame's local variable slot on the stack.\n    fn store_cell(&mut self, cached_frame: &CachedFrame<'_>, slot: u16) {\n        let value = self.pop();\n        // The guard will clean up the new value if we panic, or the old value if we swap\n        let mut guard = HeapGuard::new(value, self);\n        let (value, this) = guard.as_parts_mut();\n\n        let cell_id = this.cell_id_from_local(cached_frame, slot);\n        match this.heap.get_mut(cell_id) {\n            HeapDataMut::Cell(c) => std::mem::swap(&mut c.0, value),\n            _ => panic!(\"StoreCell: entry is not a Cell\"),\n        }\n    }\n}\n\n// `heap` is not a public field on VM, so this implementation needs to go here rather than in `heap.rs`\nimpl<T: ResourceTracker> ContainsHeap for VM<'_, '_, T> {\n    type ResourceTracker = T;\n    fn heap(&self) -> &Heap<T> {\n        self.heap\n    }\n    fn heap_mut(&mut self) -> &mut Heap<T> {\n        self.heap\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/bytecode/vm/scheduler.rs",
    "content": "//! Task scheduler for async execution and call ID allocation.\n//!\n//! # Task Model\n//!\n//! - Task 0 is the \"main task\" which uses the VM's stack/frames directly\n//! - Spawned tasks (1+) store their own execution context in the Task struct\n//! - When switching tasks, the scheduler swaps contexts with the VM\n\nuse std::collections::VecDeque;\n\nuse ahash::{AHashMap, AHashSet};\n\nuse crate::{\n    args::ArgValues,\n    asyncio::{CallId, TaskId},\n    exception_private::RunError,\n    heap::{DropWithHeap, HeapId},\n    heap_data::HeapDataMut,\n    parse::CodeRange,\n    value::Value,\n};\n\n/// Task execution state for async scheduling.\n///\n/// Tracks whether a task is ready to run, blocked waiting for something,\n/// or has completed (successfully or with an error).\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum TaskState {\n    /// Task is ready to execute (in the ready queue).\n    Ready,\n    /// Task is blocked waiting for an external call to resolve.\n    BlockedOnCall(CallId),\n    /// Task is blocked waiting for a GatherFuture to complete.\n    BlockedOnGather(HeapId),\n    /// Task completed successfully with a return value.\n    Completed(Value),\n    /// Task failed with an error.\n    Failed(RunError),\n}\n\n/// A single async task with its own execution context.\n///\n/// The main task (task 0) doesn't store its own frames/stack - it uses the VM's\n/// directly. Spawned tasks store their execution context here so they can be\n/// swapped in and out.\n///\n/// # Context Switching\n///\n/// When switching away from a non-main task, its context is saved here.\n/// When switching to it, the context is loaded into the VM.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct Task {\n    /// Unique identifier for this task.\n    pub id: TaskId,\n    /// Serialized call frames for this task's execution.\n    /// Empty for the main task (which uses VM's frames directly).\n    pub frames: Vec<SerializedTaskFrame>,\n    /// Operand stack for this task.\n    /// Empty for the main task (which uses VM's stack directly).\n    pub stack: Vec<Value>,\n    /// Exception stack for nested except blocks.\n    pub exception_stack: Vec<Value>,\n    /// VM-level instruction_ip (for exception table lookup).\n    pub instruction_ip: usize,\n    /// Coroutine being executed by this task (if any).\n    /// Used to mark the coroutine as Completed when the task finishes.\n    pub coroutine_id: Option<HeapId>,\n    /// GatherFuture this task belongs to (if spawned by gather).\n    /// Used to cancel sibling tasks when this task fails.\n    pub gather_id: Option<HeapId>,\n    /// Index in the gather's results where this task's result should be stored.\n    /// Only set for tasks spawned by gather.\n    pub gather_result_idx: Option<usize>,\n    /// Current execution state.\n    pub state: TaskState,\n    /// CallId that unblocked this task (set when task transitions from Blocked to Ready).\n    /// Used to retrieve the resolved value when the task resumes.\n    pub unblocked_by: Option<CallId>,\n}\n\n/// Serialized call frame for task storage.\n///\n/// Similar to `SerializedFrame` but used within the scheduler for task context.\n/// Cannot store `&Code` references - uses `FunctionId` to look up code on resume.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) struct SerializedTaskFrame {\n    /// Which function's code this frame executes (None = module-level).\n    pub function_id: Option<crate::intern::FunctionId>,\n    /// Instruction pointer within this frame's bytecode.\n    pub ip: usize,\n    /// Base index into the VM stack for this frame's locals region.\n    pub stack_base: usize,\n    /// Number of local variable slots (0 for module-level frames).\n    pub locals_count: u16,\n    /// Call site position (for tracebacks).\n    pub call_position: Option<CodeRange>,\n}\n\nimpl Task {\n    /// Creates a new task in the Ready state.\n    ///\n    /// # Arguments\n    /// * `id` - Unique task identifier\n    /// * `coroutine_id` - Optional HeapId of the coroutine being executed\n    /// * `gather_id` - Optional HeapId of the GatherFuture this task belongs to\n    pub fn new(\n        id: TaskId,\n        coroutine_id: Option<HeapId>,\n        gather_id: Option<HeapId>,\n        gather_result_idx: Option<usize>,\n    ) -> Self {\n        Self {\n            id,\n            frames: Vec::new(),\n            stack: Vec::new(),\n            exception_stack: Vec::new(),\n            instruction_ip: 0,\n            coroutine_id,\n            gather_id,\n            gather_result_idx,\n            state: TaskState::Ready,\n            unblocked_by: None,\n        }\n    }\n\n    /// Returns true if this task has completed (successfully or with failure).\n    #[inline]\n    pub fn is_finished(&self) -> bool {\n        matches!(self.state, TaskState::Completed(_) | TaskState::Failed(_))\n    }\n}\n\n/// Internal representation of a pending external call.\n///\n/// Stores the data needed to retry or resume an external function call,\n/// along with tracking information for the task that created it.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct PendingCallData {\n    /// Arguments for the function (includes both positional and keyword args).\n    pub args: ArgValues,\n    /// Task that created this call (for ignoring results if task is cancelled).\n    pub creator_task: TaskId,\n}\n\n/// Scheduler for managing call IDs, async tasks, and external call tracking.\n///\n/// Always present on the VM (not optional). Owns the `next_call_id` counter\n/// used by both sync and async code paths, plus all async-related state:\n/// - Task management (creation, scheduling, completion)\n/// - External call tracking and resolution\n///\n/// # Main Task\n///\n/// Task 0 is the \"main task\" which executes using the VM's stack/frames directly.\n/// It's always created at scheduler initialization but doesn't store its own context\n/// (the VM holds it). Spawned tasks (1+) store their context in the Task struct.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct Scheduler {\n    /// All tasks (main task at index 0, spawned tasks follow).\n    tasks: Vec<Task>,\n    /// Queue of task IDs ready to execute.\n    ready_queue: VecDeque<TaskId>,\n    /// Currently executing task (None only during task switching).\n    current_task: Option<TaskId>,\n    /// Counter for generating new task IDs.\n    next_task_id: u32,\n    /// Counter for external call IDs (always incremented, even for sync resolution).\n    next_call_id: u32,\n    /// Maps CallId -> pending call data for unresolved external calls.\n    /// Populated when host calls `run_pending()`.\n    pending_calls: AHashMap<CallId, PendingCallData>,\n    /// Maps CallId -> resolved Value for futures that have been resolved.\n    /// Entry is removed when the value is consumed by awaiting.\n    resolved: AHashMap<CallId, Value>,\n    /// CallIds that have been awaited (to detect double-await).\n    consumed: AHashSet<CallId>,\n    /// Maps CallId -> (gather_heap_id, result_index) for gathers waiting on external futures.\n    /// When a CallId is resolved, the result is stored in the gather's results at the given index.\n    gather_waiters: AHashMap<CallId, (HeapId, usize)>,\n}\n\nimpl Scheduler {\n    /// Creates a new scheduler with the main task (task 0) as current.\n    ///\n    /// The main task uses the VM's stack/frames directly and is always present.\n    /// It starts as the current task (not in the ready queue) since it runs\n    /// immediately without needing to be scheduled.\n    pub fn new() -> Self {\n        let mut main_task = Task::new(TaskId::default(), None, None, None);\n        // Main task starts Running, not Ready (it's the current task, not waiting)\n        main_task.state = TaskState::Ready; // Will be set properly when it blocks\n        Self {\n            tasks: vec![main_task],\n            ready_queue: VecDeque::new(), // Main task is current, not in ready queue\n            current_task: Some(TaskId::default()),\n            next_task_id: 1,\n            next_call_id: 0,\n            pending_calls: AHashMap::new(),\n            resolved: AHashMap::new(),\n            consumed: AHashSet::new(),\n            gather_waiters: AHashMap::new(),\n        }\n    }\n\n    /// Returns the currently executing task ID.\n    ///\n    /// Returns `None` only during task switching operations.\n    #[inline]\n    pub fn current_task_id(&self) -> Option<TaskId> {\n        self.current_task\n    }\n\n    /// Returns a reference to a task by ID.\n    ///\n    /// # Panics\n    /// Panics if the task ID doesn't exist.\n    #[inline]\n    pub fn get_task(&self, task_id: TaskId) -> &Task {\n        &self.tasks[task_id.raw() as usize]\n    }\n\n    /// Returns a mutable reference to a task by ID.\n    ///\n    /// # Panics\n    /// Panics if the task ID doesn't exist.\n    #[inline]\n    pub fn get_task_mut(&mut self, task_id: TaskId) -> &mut Task {\n        &mut self.tasks[task_id.raw() as usize]\n    }\n\n    /// Allocates a new CallId for an external function call.\n    ///\n    /// The counter always increments, even for sync resolution, to keep IDs unique.\n    pub fn allocate_call_id(&mut self) -> CallId {\n        let id = CallId::new(self.next_call_id);\n        self.next_call_id += 1;\n        id\n    }\n\n    /// Stores pending call data for an external function call.\n    ///\n    /// Called when the host uses async resolution (`run_pending()`).\n    pub fn add_pending_call(&mut self, call_id: CallId, data: PendingCallData) {\n        self.pending_calls.insert(call_id, data);\n    }\n\n    /// Removes a call_id from the pending_calls map.\n    ///\n    /// Called when resolving a gather's external future - the call is no longer\n    /// pending once the result has been stored in the gather's results.\n    pub fn remove_pending_call(&mut self, call_id: CallId) {\n        self.pending_calls.remove(&call_id);\n    }\n\n    /// Returns true if a CallId has already been awaited (consumed).\n    #[inline]\n    pub fn is_consumed(&self, call_id: CallId) -> bool {\n        self.consumed.contains(&call_id)\n    }\n\n    /// Marks a CallId as consumed (awaited).\n    pub fn mark_consumed(&mut self, call_id: CallId) {\n        self.consumed.insert(call_id);\n    }\n\n    /// Registers a gather as waiting on an external future.\n    ///\n    /// When the CallId is resolved, the result will be stored in the gather's results\n    /// at the specified index.\n    pub fn register_gather_for_call(&mut self, call_id: CallId, gather_id: HeapId, result_index: usize) {\n        self.gather_waiters.insert(call_id, (gather_id, result_index));\n    }\n\n    /// Returns gather info if a gather is waiting on this CallId.\n    ///\n    /// Returns (gather_heap_id, result_index) if found, None otherwise.\n    /// Removes the entry from gather_waiters.\n    pub fn take_gather_waiter(&mut self, call_id: CallId) -> Option<(HeapId, usize)> {\n        self.gather_waiters.remove(&call_id)\n    }\n\n    /// Resolves a CallId with a value.\n    ///\n    /// Stores the value for later retrieval when the future is awaited.\n    /// If a task is blocked on this call, it will be unblocked.\n    ///\n    /// Uses `pending_calls` for O(1) lookup of the blocked task instead of\n    /// scanning all tasks.\n    pub fn resolve(&mut self, call_id: CallId, value: Value) {\n        // Get blocked task from pending_calls before removing (O(1) lookup)\n        let blocked_task = self.pending_calls.remove(&call_id).map(|data| data.creator_task);\n\n        // Store the resolved value\n        self.resolved.insert(call_id, value);\n\n        // Unblock the task if found\n        if let Some(task_id) = blocked_task {\n            let task = self.get_task_mut(task_id);\n            if matches!(task.state, TaskState::BlockedOnCall(cid) if cid == call_id) {\n                task.state = TaskState::Ready;\n                task.unblocked_by = Some(call_id);\n                self.ready_queue.push_back(task_id);\n            }\n        }\n    }\n\n    /// Takes the resolved value for a CallId, if available.\n    ///\n    /// Removes the value from the resolved map and returns it.\n    /// Returns `None` if the call hasn't been resolved yet.\n    pub fn take_resolved(&mut self, call_id: CallId) -> Option<Value> {\n        self.resolved.remove(&call_id)\n    }\n\n    /// Takes the resolved value for a task that was unblocked.\n    ///\n    /// If the task has an `unblocked_by` CallId set, takes the resolved value\n    /// for that call and clears the `unblocked_by` field.\n    /// Returns `None` if the task wasn't unblocked by a resolved call.\n    pub fn take_resolved_for_task(&mut self, task_id: TaskId) -> Option<Value> {\n        let task = &mut self.tasks[task_id.raw() as usize];\n        if let Some(call_id) = task.unblocked_by.take() {\n            self.resolved.remove(&call_id)\n        } else {\n            None\n        }\n    }\n\n    /// Marks the current task as blocked on an external call.\n    ///\n    /// The task will be unblocked when `resolve()` is called with the matching CallId.\n    pub fn block_current_on_call(&mut self, call_id: CallId) {\n        if let Some(task_id) = self.current_task {\n            let task = self.get_task_mut(task_id);\n            task.state = TaskState::BlockedOnCall(call_id);\n        }\n    }\n\n    /// Marks the current task as blocked on a GatherFuture.\n    ///\n    /// The task will be unblocked when all gathered tasks complete.\n    pub fn block_current_on_gather(&mut self, gather_id: HeapId) {\n        if let Some(task_id) = self.current_task {\n            let task = self.get_task_mut(task_id);\n            task.state = TaskState::BlockedOnGather(gather_id);\n        }\n    }\n\n    /// Returns all pending (unresolved) CallIds.\n    pub fn pending_call_ids(&self) -> Vec<CallId> {\n        self.pending_calls.keys().copied().collect()\n    }\n\n    /// Removes a task from the ready queue.\n    ///\n    /// Used when handling the main task directly (via `prepare_main_task_after_resolve`)\n    /// instead of through the normal task switching mechanism.\n    pub fn remove_from_ready_queue(&mut self, task_id: TaskId) {\n        self.ready_queue.retain(|&id| id != task_id);\n    }\n\n    /// Spawns a new task from a coroutine.\n    ///\n    /// Creates a new task that will execute the given coroutine when scheduled.\n    /// The task is added to the ready queue.\n    ///\n    /// # Arguments\n    /// * `coroutine_id` - HeapId of the coroutine to execute\n    /// * `gather_id` - Optional HeapId of the GatherFuture this task belongs to\n    /// * `gather_result_idx` - Optional index in the gather's results for this task\n    ///\n    /// # Returns\n    /// The TaskId of the newly created task.\n    pub fn spawn(\n        &mut self,\n        coroutine_id: HeapId,\n        gather_id: Option<HeapId>,\n        gather_result_idx: Option<usize>,\n    ) -> TaskId {\n        let task_id = TaskId::new(self.next_task_id);\n        self.next_task_id += 1;\n\n        let task = Task::new(task_id, Some(coroutine_id), gather_id, gather_result_idx);\n        self.tasks.push(task);\n        self.ready_queue.push_back(task_id);\n\n        task_id\n    }\n\n    /// Gets the next ready task from the queue.\n    ///\n    /// Returns `None` if no tasks are ready.\n    pub fn next_ready_task(&mut self) -> Option<TaskId> {\n        self.ready_queue.pop_front()\n    }\n\n    /// Adds a task back to the ready queue.\n    pub fn make_ready(&mut self, task_id: TaskId) {\n        let task = self.get_task_mut(task_id);\n        task.state = TaskState::Ready;\n        self.ready_queue.push_back(task_id);\n    }\n\n    /// Sets the current task.\n    pub fn set_current_task(&mut self, task_id: Option<TaskId>) {\n        self.current_task = task_id;\n    }\n\n    /// Marks a task as completed with a result value.\n    ///\n    /// If the task is part of a gather, updates the gather's results.\n    /// If this completes the gather, unblocks the waiting task.\n    pub fn complete_task(&mut self, task_id: TaskId, result: Value) {\n        let task = self.get_task_mut(task_id);\n        task.state = TaskState::Completed(result);\n        // Note: gather wake-up logic will be implemented when gather is fully integrated\n    }\n\n    /// Marks a task as failed with an error.\n    ///\n    /// If the task is part of a gather, returns the gather_id so the caller\n    /// can collect siblings from `GatherFuture.task_ids` on the heap.\n    ///\n    /// # Returns\n    /// The gather_id if this task belongs to a gather (for sibling lookup).\n    pub fn fail_task(&mut self, task_id: TaskId, error: RunError) -> Option<HeapId> {\n        let task = self.get_task_mut(task_id);\n        let gather_id = task.gather_id;\n        task.state = TaskState::Failed(error);\n        gather_id\n    }\n\n    /// Cancels a task, cleaning up its resources.\n    ///\n    /// This marks the task as Failed with a cancellation error and cleans up:\n    /// - Stack values\n    /// - Exception stack values\n    /// - Frame cell references\n    /// - Frame namespaces\n    /// - Nested gathers (if the task was blocked on one)\n    /// - Completed task results (if task finished before cancellation)\n    ///\n    /// The caller is responsible for cleaning up the task's coroutine on the heap.\n    ///\n    /// # Arguments\n    /// * `task_id` - ID of the task to cancel\n    /// * `heap` - Heap for dropping values and cell cleanup\n    pub fn cancel_task(\n        &mut self,\n        task_id: TaskId,\n        heap: &mut crate::heap::Heap<impl crate::resource::ResourceTracker>,\n    ) {\n        // If task already finished, clean up its result value and return\n        if self.get_task(task_id).is_finished() {\n            let task = self.get_task_mut(task_id);\n            if let TaskState::Completed(value) = std::mem::replace(&mut task.state, TaskState::Ready) {\n                value.drop_with_heap(heap);\n            }\n            // Note: Failed tasks don't have values to clean up (RunError doesn't contain Values)\n            return;\n        }\n\n        // Remove from ready queue if present (do this before getting mutable task reference)\n        self.ready_queue.retain(|&id| id != task_id);\n\n        // Check if task is blocked on a gather and get the gather info before mutating task\n        let inner_gather_info = {\n            let task = self.get_task(task_id);\n            if let TaskState::BlockedOnGather(gather_id) = task.state {\n                // Get inner gather's task IDs from heap\n                if let crate::heap::HeapData::GatherFuture(gather) = heap.get(gather_id) {\n                    Some((gather_id, gather.task_ids.clone()))\n                } else {\n                    None\n                }\n            } else {\n                None\n            }\n        };\n\n        // Recursively cancel inner gather's tasks first\n        if let Some((inner_gather_id, inner_task_ids)) = inner_gather_info {\n            for inner_task_id in inner_task_ids {\n                self.cancel_task(inner_task_id, heap);\n            }\n\n            // Cleanup the inner GatherFuture - extract data first to avoid borrow conflict\n            let (items, results) = if let HeapDataMut::GatherFuture(gather) = heap.get_mut(inner_gather_id) {\n                (std::mem::take(&mut gather.items), std::mem::take(&mut gather.results))\n            } else {\n                (vec![], vec![])\n            };\n\n            // Now cleanup the extracted data with mutable heap access\n            for item in items {\n                if let crate::asyncio::GatherItem::Coroutine(coro_id) = item {\n                    heap.dec_ref(coro_id);\n                }\n            }\n            for value in results.into_iter().flatten() {\n                value.drop_with_heap(heap);\n            }\n\n            // Dec_ref the gather itself\n            heap.dec_ref(inner_gather_id);\n        }\n\n        // Now get mutable reference to the task for cleanup\n        let task = self.get_task_mut(task_id);\n\n        // Clean up stack values\n        for value in std::mem::take(&mut task.stack) {\n            value.drop_with_heap(heap);\n        }\n\n        // Clean up exception stack values\n        for value in std::mem::take(&mut task.exception_stack) {\n            value.drop_with_heap(heap);\n        }\n\n        // Restore this task's depth contribution before cleanup,\n        // since save_task_context subtracted it.\n        let task_depth = task.frames.len();\n        let global_depth = heap.get_recursion_depth();\n        heap.set_recursion_depth(global_depth + task_depth);\n        task.frames.clear();\n\n        // Mark as failed with a cancellation error\n        task.state = TaskState::Failed(\n            crate::exception_private::SimpleException::new_msg(\n                crate::exception_private::ExcType::RuntimeError,\n                \"task was cancelled\",\n            )\n            .into(),\n        );\n    }\n\n    /// Fails the task blocked on a specific CallId with an error.\n    ///\n    /// Used when an external function returns an error via `FutureSnapshot::resume`.\n    /// Uses `pending_calls` for O(1) lookup of the blocked task.\n    ///\n    /// # Returns\n    /// A tuple of (task_id, gather_id) if a task was found,\n    /// or None if no task was blocked on this CallId.\n    /// Callers should get siblings from `GatherFuture.task_ids` if gather_id is Some.\n    pub fn fail_for_call(&mut self, call_id: CallId, error: RunError) -> Option<(TaskId, Option<HeapId>)> {\n        // Get blocked task from pending_calls (O(1) lookup)\n        let task_id = self.pending_calls.remove(&call_id)?.creator_task;\n        let gather_id = self.fail_task(task_id, error);\n        Some((task_id, gather_id))\n    }\n\n    /// Returns the task that created a specific pending call.\n    ///\n    /// Used to check if a pending call's creator task has been cancelled.\n    #[inline]\n    pub fn get_pending_call_creator(&self, call_id: CallId) -> Option<TaskId> {\n        self.pending_calls.get(&call_id).map(|data| data.creator_task)\n    }\n\n    /// Returns true if a task has been cancelled or failed.\n    #[inline]\n    pub fn is_task_failed(&self, task_id: TaskId) -> bool {\n        matches!(self.tasks.get(task_id.raw() as usize), Some(task) if matches!(task.state, TaskState::Failed(_)))\n    }\n\n    /// Cleans up all scheduler resources: pending calls, resolved values, task\n    /// stacks/exception stacks, completed results, and task frame cell references.\n    ///\n    /// Each task's `recursion_depth` is restored to the global counter before\n    /// dropping cells, because `save_task_context` subtracted the recursion depth\n    /// and cleanup needs the correct depth to avoid underflow.\n    pub fn cleanup(&mut self, heap: &mut crate::heap::Heap<impl crate::resource::ResourceTracker>) {\n        // Drop pending call arguments\n        for (_, data) in std::mem::take(&mut self.pending_calls) {\n            data.args.drop_with_heap(heap);\n        }\n        // Drop resolved values\n        for (_, value) in std::mem::take(&mut self.resolved) {\n            value.drop_with_heap(heap);\n        }\n        // Drop task stack/exception values and completed results\n        for task in &mut self.tasks {\n            for value in std::mem::take(&mut task.stack) {\n                value.drop_with_heap(heap);\n            }\n            for value in std::mem::take(&mut task.exception_stack) {\n                value.drop_with_heap(heap);\n            }\n            if let TaskState::Completed(value) = std::mem::replace(&mut task.state, TaskState::Ready) {\n                value.drop_with_heap(heap);\n            }\n            // Restore recursion depth and clear frames\n            let task_depth = task.frames.len();\n            let global_depth = heap.get_recursion_depth();\n            heap.set_recursion_depth(global_depth + task_depth);\n            task.frames.clear();\n        }\n    }\n}\n\nimpl Default for Scheduler {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/exception_private.rs",
    "content": "use std::{\n    borrow::Cow,\n    fmt::{self, Display, Write},\n};\n\nuse serde::{Deserialize, Serialize};\nuse smallvec::smallvec;\nuse strum::{Display, EnumString, IntoStaticStr};\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_public::{MontyException, StackFrame},\n    fstring::FormatError,\n    heap::{Heap, HeapData},\n    intern::{Interns, StaticStrings, StringId},\n    parse::CodeRange,\n    resource::ResourceTracker,\n    types::{\n        PyTrait, Str, Type, allocate_tuple,\n        str::{StringRepr, string_repr_fmt},\n    },\n    value::{EitherStr, Value},\n};\n\n/// Result type alias for operations that can produce a runtime error.\npub type RunResult<T> = Result<T, RunError>;\n\n/// Python exception types supported by the interpreter.\n///\n/// Uses strum derives for automatic `Display`, `FromStr`, and `Into<&'static str>` implementations.\n/// The string representation matches the variant name exactly (e.g., `ValueError` -> \"ValueError\").\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, IntoStaticStr, Serialize, Deserialize)]\npub enum ExcType {\n    /// primary exception class - matches any exception in isinstance checks.\n    Exception,\n\n    /// System exit exceptions\n    BaseException,\n    SystemExit,\n    KeyboardInterrupt,\n\n    // --- ArithmeticError hierarchy ---\n    /// Intermediate class for arithmetic errors.\n    ArithmeticError,\n    /// Subclass of ArithmeticError.\n    OverflowError,\n    /// Subclass of ArithmeticError.\n    ZeroDivisionError,\n\n    // --- LookupError hierarchy ---\n    /// Intermediate class for lookup errors.\n    LookupError,\n    /// Subclass of LookupError.\n    IndexError,\n    /// Subclass of LookupError.\n    KeyError,\n\n    // --- RuntimeError hierarchy ---\n    /// Intermediate class for runtime errors.\n    RuntimeError,\n    /// Subclass of RuntimeError.\n    NotImplementedError,\n    /// Subclass of RuntimeError.\n    RecursionError,\n\n    // --- AttributeError hierarchy ---\n    AttributeError,\n    /// Subclass of AttributeError (from dataclasses module).\n    FrozenInstanceError,\n\n    // --- NameError hierarchy ---\n    NameError,\n    /// Subclass of NameError - for accessing local variable before assignment.\n    UnboundLocalError,\n\n    // --- ValueError hierarchy ---\n    ValueError,\n    /// Subclass of ValueError - for encoding/decoding errors.\n    UnicodeDecodeError,\n\n    // --- ImportError hierarchy ---\n    /// Import-related errors (module not found, name not in module).\n    ImportError,\n    /// Subclass of ImportError - for when a module cannot be found.\n    ModuleNotFoundError,\n\n    // --- OSError hierarchy ---\n    /// OS-related errors (file not found, permission denied, etc.)\n    OSError,\n    /// Subclass of OSError - for when a file or directory cannot be found.\n    FileNotFoundError,\n    /// Subclass of OSError - for when a file already exists.\n    FileExistsError,\n    /// Subclass of OSError - for when a path is a directory but a file was expected.\n    IsADirectoryError,\n    /// Subclass of OSError - for when a path is not a directory but one was expected.\n    NotADirectoryError,\n\n    // --- Standalone exception types ---\n    AssertionError,\n    MemoryError,\n    StopIteration,\n    SyntaxError,\n    TimeoutError,\n    TypeError,\n\n    // --- Module-specific exception types ---\n\n    // --- re module ---\n    /// `re.PatternError` - raised for invalid regex patterns or unsupported regex features.\n    ///\n    /// # Behavior Note\n    ///\n    /// Limited to monty's exception type, `PatternError` does not provide `pattern`, `pos`,\n    /// `lineno` and `colno` attributes.\n    ///\n    /// As per CPython's implementation, it would be hard to convert `fancy-regex`'s error\n    /// representations into the required attributes.\n    #[strum(serialize = \"re.PatternError\")]\n    RePatternError,\n}\n\nimpl ExcType {\n    /// Checks if this exception type is a subclass of another exception type.\n    ///\n    /// Implements Python's exception hierarchy for try/except matching:\n    /// - `Exception` is the base class for all standard exceptions\n    /// - `LookupError` is the base for `KeyError` and `IndexError`\n    /// - `ArithmeticError` is the base for `ZeroDivisionError` and `OverflowError`\n    /// - `RuntimeError` is the base for `RecursionError` and `NotImplementedError`\n    ///\n    /// Returns true if `self` would be caught by `except handler_type:`.\n    #[must_use]\n    pub fn is_subclass_of(self, handler_type: Self) -> bool {\n        if self == handler_type {\n            return true;\n        }\n        match handler_type {\n            // BaseException catches all exceptions\n            Self::BaseException => true,\n            // Exception catches everything except BaseException, and direct subclasses: KeyboardInterrupt, SystemExit\n            Self::Exception => !matches!(self, Self::BaseException | Self::KeyboardInterrupt | Self::SystemExit),\n            // LookupError catches KeyError and IndexError\n            Self::LookupError => matches!(self, Self::KeyError | Self::IndexError),\n            // ArithmeticError catches ZeroDivisionError and OverflowError\n            Self::ArithmeticError => matches!(self, Self::ZeroDivisionError | Self::OverflowError),\n            // RuntimeError catches RecursionError and NotImplementedError\n            Self::RuntimeError => matches!(self, Self::RecursionError | Self::NotImplementedError),\n            // AttributeError catches FrozenInstanceError\n            Self::AttributeError => matches!(self, Self::FrozenInstanceError),\n            // NameError catches UnboundLocalError\n            Self::NameError => matches!(self, Self::UnboundLocalError),\n            // ValueError catches UnicodeDecodeError\n            Self::ValueError => matches!(self, Self::UnicodeDecodeError),\n            // ImportError catches ModuleNotFoundError\n            Self::ImportError => matches!(self, Self::ModuleNotFoundError),\n            // OSError catches FileNotFoundError, FileExistsError, IsADirectoryError, NotADirectoryError\n            Self::OSError => matches!(\n                self,\n                Self::FileNotFoundError | Self::FileExistsError | Self::IsADirectoryError | Self::NotADirectoryError\n            ),\n            // All other types only match exactly (handled by self == handler_type above)\n            _ => false,\n        }\n    }\n\n    /// Creates an exception instance from an exception type and arguments.\n    ///\n    /// Handles exception constructors like `ValueError('message')`.\n    /// Currently supports zero or one string argument.\n    ///\n    /// The `interns` parameter provides access to interned string content.\n    /// Returns a heap-allocated exception value.\n    pub(crate) fn call(self, vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        defer_drop!(args, vm);\n        let exc = match args {\n            ArgValues::Empty => Ok(SimpleException::new_none(self)),\n            ArgValues::One(value) => match value {\n                Value::InternString(string_id) => Ok(SimpleException::new_msg(\n                    self,\n                    vm.interns.get_str(*string_id).to_owned(),\n                )),\n                Value::Ref(heap_id) => {\n                    if let HeapData::Str(s) = vm.heap.get(*heap_id) {\n                        Ok(SimpleException::new_msg(self, s.as_str().to_owned()))\n                    } else {\n                        Err(RunError::internal(\n                            \"exceptions can only be called with zero or one string argument\",\n                        ))\n                    }\n                }\n                _ => Err(RunError::internal(\n                    \"exceptions can only be called with zero or one string argument\",\n                )),\n            },\n            _ => Err(RunError::internal(\n                \"exceptions can only be called with zero or one string argument\",\n            )),\n        }?;\n        let heap_id = vm.heap.allocate(HeapData::Exception(exc))?;\n        Ok(Value::Ref(heap_id))\n    }\n\n    /// Creates an AttributeError for when an attribute is not found (GET operation).\n    ///\n    /// Sets `hide_caret: true` because CPython doesn't show carets for attribute GET errors.\n    #[must_use]\n    pub(crate) fn attribute_error(type_name: impl Display, attr: &str) -> RunError {\n        let exc = SimpleException::new_msg(\n            Self::AttributeError,\n            format!(\"'{type_name}' object has no attribute '{attr}'\"),\n        );\n        RunError::Exc(ExceptionRaise {\n            exc,\n            frame: None,\n            hide_caret: true, // CPython doesn't show carets for attribute GET errors\n        })\n    }\n\n    /// Creates an AttributeError for attribute assignment on types that don't support it.\n    ///\n    /// Matches CPython's format for setting attributes on built-in types.\n    #[must_use]\n    pub(crate) fn attribute_error_no_setattr(type_: Type, attr_name: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::AttributeError,\n            format!(\"'{type_}' object has no attribute '{attr_name}' and no __dict__ for setting new attributes\"),\n        )\n        .into()\n    }\n\n    /// Creates an AttributeError for a missing module attribute.\n    ///\n    /// Matches CPython's format: `AttributeError: module 'name' has no attribute 'attr'`\n    /// Sets `hide_caret: true` because CPython doesn't show carets for attribute GET errors.\n    #[must_use]\n    pub(crate) fn attribute_error_module(module_name: &str, attr_name: &str) -> RunError {\n        let exc = SimpleException::new_msg(\n            Self::AttributeError,\n            format!(\"module '{module_name}' has no attribute '{attr_name}'\"),\n        );\n        RunError::Exc(ExceptionRaise {\n            exc,\n            frame: None,\n            hide_caret: true, // CPython doesn't show carets for attribute GET errors\n        })\n    }\n\n    /// Creates a FrozenInstanceError for assigning to a frozen dataclass.\n    ///\n    /// Matches CPython's `dataclasses.FrozenInstanceError` which is a subclass of `AttributeError`.\n    /// Message format: \"cannot assign to field 'attr_name'\"\n    #[must_use]\n    pub(crate) fn frozen_instance_error(attr_name: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::FrozenInstanceError,\n            format!(\"cannot assign to field '{attr_name}'\"),\n        )\n        .into()\n    }\n\n    #[must_use]\n    pub(crate) fn type_error_not_sub(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"'{type_}' object is not subscriptable\")).into()\n    }\n\n    /// Creates a TypeError for awaiting a non-awaitable object.\n    ///\n    /// Matches CPython's format: `TypeError: '{type}' object can't be awaited`\n    #[must_use]\n    pub(crate) fn object_not_awaitable(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"'{type_}' object can't be awaited\")).into()\n    }\n\n    /// Creates a TypeError for item assignment on types that don't support it.\n    ///\n    /// Matches CPython's format: `TypeError: '{type}' object does not support item assignment`\n    #[must_use]\n    pub(crate) fn type_error_not_sub_assignment(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"'{type_}' object does not support item assignment\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for unhashable types when calling `hash()`.\n    ///\n    /// This matches Python 3.14's error message: `TypeError: unhashable type: 'list'`\n    #[must_use]\n    pub(crate) fn type_error_unhashable(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"unhashable type: '{type_}'\")).into()\n    }\n\n    /// Creates a TypeError for unhashable types used as dict keys.\n    ///\n    /// This matches Python 3.14's error message:\n    /// `TypeError: cannot use 'list' as a dict key (unhashable type: 'list')`\n    #[must_use]\n    pub(crate) fn type_error_unhashable_dict_key(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"cannot use '{type_}' as a dict key (unhashable type: '{type_}')\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for unhashable types used as set elements.\n    ///\n    /// This matches Python 3.14's error message:\n    /// `TypeError: cannot use 'list' as a set element (unhashable type: 'list')`\n    #[must_use]\n    pub(crate) fn type_error_unhashable_set_element(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"cannot use '{type_}' as a set element (unhashable type: '{type_}')\"),\n        )\n        .into()\n    }\n\n    /// Creates a KeyError for a missing dict key.\n    ///\n    /// For string keys, uses the raw string value without extra quoting.\n    #[must_use]\n    pub(crate) fn key_error(key: &Value, vm: &VM<'_, '_, impl ResourceTracker>) -> RunError {\n        let key_str = key.py_str(vm).into_owned();\n        SimpleException::new_msg(Self::KeyError, key_str).into()\n    }\n\n    /// Creates a KeyError for popping from an empty set.\n    ///\n    /// Matches CPython's error format: `KeyError: 'pop from an empty set'`\n    #[must_use]\n    pub(crate) fn key_error_pop_empty_set() -> RunError {\n        SimpleException::new_msg(Self::KeyError, \"pop from an empty set\").into()\n    }\n\n    /// Creates a TypeError for when a function receives the wrong number of arguments.\n    ///\n    /// Matches CPython's error format exactly:\n    /// - For 1 expected arg: `{name}() takes exactly one argument ({actual} given)`\n    /// - For N expected args: `{name} expected {expected} arguments, got {actual}`\n    ///\n    /// # Arguments\n    /// * `name` - The function name (e.g., \"len\" for builtins, \"list.append\" for methods)\n    /// * `expected` - Number of expected arguments\n    /// * `actual` - Number of arguments actually provided\n    #[must_use]\n    pub(crate) fn type_error_arg_count(name: &str, expected: usize, actual: usize) -> RunError {\n        if expected == 1 {\n            // CPython: \"len() takes exactly one argument (2 given)\"\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() takes exactly one argument ({actual} given)\"),\n            )\n            .into()\n        } else {\n            // CPython: \"insert expected 2 arguments, got 1\"\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name} expected {expected} arguments, got {actual}\"),\n            )\n            .into()\n        }\n    }\n\n    /// Creates a TypeError for when a method that takes no arguments receives some.\n    ///\n    /// Matches CPython's format: `{name}() takes no arguments ({actual} given)`\n    ///\n    /// # Arguments\n    /// * `name` - The method name (e.g., \"dict.keys\")\n    /// * `actual` - Number of arguments actually provided\n    #[must_use]\n    pub(crate) fn type_error_no_args(name: &str, actual: usize) -> RunError {\n        // CPython: \"dict.keys() takes no arguments (1 given)\"\n        SimpleException::new_msg(Self::TypeError, format!(\"{name}() takes no arguments ({actual} given)\")).into()\n    }\n\n    /// Creates a TypeError for when a function receives fewer arguments than required.\n    ///\n    /// Matches CPython's format: `{name} expected at least {min} argument, got {actual}`\n    ///\n    /// # Arguments\n    /// * `name` - The function name (e.g., \"get\", \"pop\")\n    /// * `min` - Minimum number of required arguments\n    /// * `actual` - Number of arguments actually provided\n    #[must_use]\n    pub(crate) fn type_error_at_least(name: &str, min: usize, actual: usize) -> RunError {\n        // CPython: \"get expected at least 1 argument, got 0\"\n        let plural = if min == 1 { \"\" } else { \"s\" };\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name} expected at least {min} argument{plural}, got {actual}\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for when a function receives more arguments than allowed.\n    ///\n    /// Matches CPython's format: `{name} expected at most {max} arguments, got {actual}`\n    ///\n    /// # Arguments\n    /// * `name` - The function name (e.g., \"get\", \"pop\")\n    /// * `max` - Maximum number of allowed arguments\n    /// * `actual` - Number of arguments actually provided\n    #[must_use]\n    pub(crate) fn type_error_at_most(name: &str, max: usize, actual: usize) -> RunError {\n        // CPython: \"get expected at most 2 arguments, got 3\"\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name} expected at most {max} arguments, got {actual}\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for missing positional arguments.\n    ///\n    /// Matches CPython's format: `{name}() missing {count} required positional argument(s): 'a' and 'b'`\n    #[must_use]\n    pub(crate) fn type_error_missing_positional_with_names(name: &str, missing_names: &[&str]) -> RunError {\n        let count = missing_names.len();\n        let names_str = format_param_names(missing_names);\n        if count == 1 {\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() missing 1 required positional argument: {names_str}\"),\n            )\n            .into()\n        } else {\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() missing {count} required positional arguments: {names_str}\"),\n            )\n            .into()\n        }\n    }\n\n    /// Creates a TypeError for missing keyword-only arguments.\n    ///\n    /// Matches CPython's format: `{name}() missing {count} required keyword-only argument(s): 'a' and 'b'`\n    #[must_use]\n    pub(crate) fn type_error_missing_kwonly_with_names(name: &str, missing_names: &[&str]) -> RunError {\n        let count = missing_names.len();\n        let names_str = format_param_names(missing_names);\n        if count == 1 {\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() missing 1 required keyword-only argument: {names_str}\"),\n            )\n            .into()\n        } else {\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() missing {count} required keyword-only arguments: {names_str}\"),\n            )\n            .into()\n        }\n    }\n\n    /// Creates a TypeError for too many positional arguments.\n    ///\n    /// Matches CPython's format:\n    /// - Simple: `{name}() takes {max} positional argument(s) but {actual} were given`\n    /// - With kwonly: `{name}() takes {max} positional argument(s) but {actual} positional argument(s) (and N keyword-only argument(s)) were given`\n    #[must_use]\n    pub(crate) fn type_error_too_many_positional(\n        name: &str,\n        max: usize,\n        actual: usize,\n        kwonly_given: usize,\n    ) -> RunError {\n        let takes_word = if max == 1 { \"argument\" } else { \"arguments\" };\n\n        if kwonly_given > 0 {\n            // CPython includes keyword-only args in the \"given\" part when present\n            let given_word = if actual == 1 { \"argument\" } else { \"arguments\" };\n            let kwonly_word = if kwonly_given == 1 { \"argument\" } else { \"arguments\" };\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\n                    \"{name}() takes {max} positional {takes_word} but {actual} positional {given_word} (and {kwonly_given} keyword-only {kwonly_word}) were given\"\n                ),\n            )\n            .into()\n        } else if max == 0 {\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() takes 0 positional arguments but {actual} were given\"),\n            )\n            .into()\n        } else {\n            SimpleException::new_msg(\n                Self::TypeError,\n                format!(\"{name}() takes {max} positional {takes_word} but {actual} were given\"),\n            )\n            .into()\n        }\n    }\n\n    /// Creates a TypeError for positional-only parameter passed as keyword.\n    ///\n    /// Matches CPython's format: `{name}() got some positional-only arguments passed as keyword arguments: '{param}'`\n    #[must_use]\n    pub(crate) fn type_error_positional_only(name: &str, param: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name}() got some positional-only arguments passed as keyword arguments: '{param}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for duplicate argument.\n    ///\n    /// Matches CPython's format: `{name}() got multiple values for argument '{param}'`\n    #[must_use]\n    pub(crate) fn type_error_duplicate_arg(name: &str, param: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name}() got multiple values for argument '{param}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for duplicate keyword argument.\n    ///\n    /// Matches CPython's format: `{name}() got multiple values for keyword argument '{key}'`\n    #[must_use]\n    pub(crate) fn type_error_multiple_values(name: &str, key: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name}() got multiple values for keyword argument '{key}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for unexpected keyword argument.\n    ///\n    /// Matches CPython's format: `{name}() got an unexpected keyword argument '{key}'`\n    #[must_use]\n    pub(crate) fn type_error_unexpected_keyword(name: &str, key: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name}() got an unexpected keyword argument '{key}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for **kwargs argument that is not a mapping.\n    ///\n    /// Matches CPython's format: `{name}() argument after ** must be a mapping, not {type_name}`\n    #[must_use]\n    pub(crate) fn type_error_kwargs_not_mapping(name: &str, type_name: &str) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{name}() argument after ** must be a mapping, not {type_name}\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for `{**x}` dict-literal unpacking where `x` is not a mapping.\n    ///\n    /// Matches CPython's format: `'{type_name}' object is not a mapping`\n    ///\n    /// Note: this differs from [`type_error_kwargs_not_mapping`] which is used for\n    /// function-call `**kwargs` and includes the function name in the message.\n    #[must_use]\n    pub(crate) fn type_error_not_mapping(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"'{type_}' object is not a mapping\")).into()\n    }\n\n    /// Creates a TypeError for **kwargs with non-string keys.\n    ///\n    /// Matches CPython's format: `{name}() keywords must be strings`\n    #[must_use]\n    pub(crate) fn type_error_kwargs_nonstring_key() -> RunError {\n        SimpleException::new_msg(Self::TypeError, \"keywords must be strings\").into()\n    }\n\n    /// Creates a simple TypeError with a custom message.\n    #[must_use]\n    pub(crate) fn type_error(msg: impl fmt::Display) -> RunError {\n        SimpleException::new_msg(Self::TypeError, msg).into()\n    }\n\n    /// Creates a TypeError for bytes() constructor with invalid type.\n    ///\n    /// Matches CPython's format: `TypeError: cannot convert '{type}' object to bytes`\n    #[must_use]\n    pub(crate) fn type_error_bytes_init(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"cannot convert '{type_}' object to bytes\")).into()\n    }\n\n    /// Creates a TypeError for calling a non-callable type.\n    ///\n    /// Matches CPython's format: `TypeError: cannot create '{type}' instances`\n    #[must_use]\n    pub(crate) fn type_error_not_callable(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"cannot create '{type_}' instances\")).into()\n    }\n\n    /// Creates a TypeError for calling a non-callable object.\n    ///\n    /// Matches CPython's format: `TypeError: '{type}' object is not callable`\n    #[must_use]\n    pub(crate) fn type_error_not_callable_object(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"'{type_}' object is not callable\")).into()\n    }\n\n    /// Creates a TypeError for non-iterable type in list/tuple/etc constructors.\n    ///\n    /// Matches CPython's format: `TypeError: '{type}' object is not iterable`\n    #[must_use]\n    pub(crate) fn type_error_not_iterable(type_: Type) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"'{type_}' object is not iterable\")).into()\n    }\n\n    /// Creates a TypeError for non-iterable type in PEP 448 `*value` literal unpack.\n    ///\n    /// Used when `[*expr]`, `(*expr,)` literal unpack encounters a non-iterable — distinct\n    /// from [`type_error_not_iterable`] because CPython uses a different message for this context.\n    ///\n    /// Matches CPython's format: `TypeError: Value after * must be an iterable, not {type}`\n    #[must_use]\n    pub(crate) fn type_error_value_after_star(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"Value after * must be an iterable, not {type_}\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for int() constructor with invalid type.\n    ///\n    /// Matches CPython's format: `TypeError: int() argument must be a string, a bytes-like object or a real number, not '{type}'`\n    #[must_use]\n    pub(crate) fn type_error_int_conversion(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"int() argument must be a string, a bytes-like object or a real number, not '{type_}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for float() constructor with invalid type.\n    ///\n    /// Matches CPython's format: `TypeError: float() argument must be a string or a real number, not '{type}'`\n    #[must_use]\n    pub(crate) fn type_error_float_conversion(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"float() argument must be a string or a real number, not '{type_}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a ValueError for negative count in bytes().\n    ///\n    /// Matches CPython's format: `ValueError: negative count`\n    #[must_use]\n    pub(crate) fn value_error_negative_bytes_count() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"negative count\").into()\n    }\n\n    /// Creates a TypeError for isinstance() arg 2.\n    ///\n    /// Matches CPython's format: `TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union`\n    #[must_use]\n    pub(crate) fn isinstance_arg2_error() -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            \"isinstance() arg 2 must be a type, a tuple of types, or a union\",\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for invalid exception type in except clause.\n    ///\n    /// Matches CPython's format: `TypeError: catching classes that do not inherit from BaseException is not allowed`\n    #[must_use]\n    pub(crate) fn except_invalid_type_error() -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            \"catching classes that do not inherit from BaseException is not allowed\",\n        )\n        .into()\n    }\n\n    /// Creates a ValueError for range() step argument being zero.\n    ///\n    /// Matches CPython's format: `ValueError: range() arg 3 must not be zero`\n    #[must_use]\n    pub(crate) fn value_error_range_step_zero() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"range() arg 3 must not be zero\").into()\n    }\n\n    /// Creates a ValueError for slice step being zero.\n    ///\n    /// Matches CPython's format: `ValueError: slice step cannot be zero`\n    #[must_use]\n    pub(crate) fn value_error_slice_step_zero() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"slice step cannot be zero\").into()\n    }\n\n    /// Creates a TypeError for slice indices that are not integers or None.\n    ///\n    /// Matches CPython's format: `TypeError: slice indices must be integers or None or have an __index__ method`\n    #[must_use]\n    pub(crate) fn type_error_slice_indices() -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            \"slice indices must be integers or None or have an __index__ method\",\n        )\n        .into()\n    }\n\n    /// Creates a RuntimeError for dict mutation during iteration.\n    ///\n    /// Matches CPython's format: `RuntimeError: dictionary changed size during iteration`\n    #[must_use]\n    pub(crate) fn runtime_error_dict_changed_size() -> RunError {\n        SimpleException::new_msg(Self::RuntimeError, \"dictionary changed size during iteration\").into()\n    }\n\n    /// Creates a RuntimeError for set mutation during iteration.\n    ///\n    /// Matches CPython's format: `RuntimeError: Set changed size during iteration`\n    #[must_use]\n    pub(crate) fn runtime_error_set_changed_size() -> RunError {\n        SimpleException::new_msg(Self::RuntimeError, \"Set changed size during iteration\").into()\n    }\n\n    /// Creates a TypeError for functions that don't accept keyword arguments.\n    ///\n    /// Matches CPython's format: `TypeError: {name}() takes no keyword arguments`\n    #[must_use]\n    pub(crate) fn type_error_no_kwargs(name: &str) -> RunError {\n        SimpleException::new_msg(Self::TypeError, format!(\"{name}() takes no keyword arguments\")).into()\n    }\n\n    /// Creates an IndexError for list index out of range (getitem).\n    ///\n    /// Matches CPython's format: `IndexError('list index out of range')`\n    #[must_use]\n    pub(crate) fn list_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"list index out of range\").into()\n    }\n\n    /// Creates an IndexError for list assignment index out of range (setitem).\n    ///\n    /// Matches CPython's format: `IndexError('list assignment index out of range')`\n    #[must_use]\n    pub(crate) fn list_assignment_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"list assignment index out of range\").into()\n    }\n\n    /// Creates an IndexError for tuple index out of range.\n    ///\n    /// Matches CPython's format: `IndexError('tuple index out of range')`\n    #[must_use]\n    pub(crate) fn tuple_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"tuple index out of range\").into()\n    }\n\n    /// Creates an IndexError for string index out of range.\n    ///\n    /// Matches CPython's format: `IndexError('string index out of range')`\n    #[must_use]\n    pub(crate) fn str_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"string index out of range\").into()\n    }\n\n    /// Creates an IndexError for bytes index out of range.\n    ///\n    /// Matches CPython's format: `IndexError('index out of range')`\n    #[must_use]\n    pub(crate) fn bytes_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"index out of range\").into()\n    }\n\n    /// Creates an IndexError for range index out of range.\n    ///\n    /// Matches CPython's format: `IndexError('range object index out of range')`\n    #[must_use]\n    pub(crate) fn range_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"range object index out of range\").into()\n    }\n\n    /// Creates an IndexError for `re.Match` group index out of range.\n    ///\n    /// Matches CPython's format: `IndexError('no such group')`\n    #[must_use]\n    pub(crate) fn re_match_group_index_error() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"no such group\").into()\n    }\n\n    /// Creates a TypeError for non-integer sequence indices (getitem).\n    ///\n    /// Matches CPython's format: `TypeError('{type}' indices must be integers, not '{index_type}')`\n    #[must_use]\n    pub(crate) fn type_error_indices(type_str: Type, index_type: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"{type_str} indices must be integers, not '{index_type}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for non-integer list indices (setitem/assignment).\n    ///\n    /// Matches CPython's format: `TypeError('list indices must be integers or slices, not {index_type}')`\n    #[must_use]\n    pub(crate) fn type_error_list_assignment_indices(index_type: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"list indices must be integers or slices, not {index_type}\"),\n        )\n        .into()\n    }\n\n    /// Creates a NameError for accessing a free variable (nonlocal/closure) before it's assigned.\n    ///\n    /// Matches CPython's format: `NameError: cannot access free variable 'x' where it is not\n    /// associated with a value in enclosing scope`\n    #[must_use]\n    pub(crate) fn name_error_free_variable(name: &str) -> SimpleException {\n        SimpleException::new_msg(\n            Self::NameError,\n            format!(\"cannot access free variable '{name}' where it is not associated with a value in enclosing scope\"),\n        )\n    }\n\n    /// Creates a NameError for accessing an undefined variable.\n    ///\n    /// Matches CPython's format: `NameError: name 'x' is not defined`\n    #[must_use]\n    pub(crate) fn name_error(name: &str) -> SimpleException {\n        let mut msg = format!(\"name '{name}' is not defined\");\n        // add the same suffix as cpython, but only for the modules supported by Monty\n        if matches!(name, \"asyncio\" | \"sys\" | \"typing\" | \"types\" | \"re\") {\n            write!(&mut msg, \". Did you forget to import '{name}'?\").unwrap();\n        }\n        SimpleException::new_msg(Self::NameError, msg)\n    }\n\n    /// Creates an UnboundLocalError for accessing a local variable before assignment.\n    ///\n    /// Matches CPython's format: `UnboundLocalError: cannot access local variable 'x' where it is not associated with a value`\n    #[must_use]\n    pub(crate) fn unbound_local_error(name: &str) -> SimpleException {\n        SimpleException::new_msg(\n            Self::UnboundLocalError,\n            format!(\"cannot access local variable '{name}' where it is not associated with a value\"),\n        )\n    }\n\n    /// Creates a ModuleNotFoundError for when a module cannot be found.\n    ///\n    /// Matches CPython's format: `ModuleNotFoundError: No module named 'name'`\n    /// Sets `hide_caret: true` because CPython doesn't show carets for module not found errors.\n    #[must_use]\n    pub(crate) fn module_not_found_error(module_name: &str) -> RunError {\n        let exc = SimpleException::new_msg(Self::ModuleNotFoundError, format!(\"No module named '{module_name}'\"));\n        RunError::Exc(ExceptionRaise {\n            exc,\n            frame: None,\n            hide_caret: true, // CPython doesn't show carets for module not found errors\n        })\n    }\n\n    /// Creates a NotImplementedError for an unimplemented Python feature.\n    ///\n    /// Used during parsing when encountering Python syntax that Monty doesn't yet support.\n    /// The message format is: \"The monty syntax parser does not yet support {feature}\"\n    #[must_use]\n    pub(crate) fn not_implemented(msg: impl fmt::Display) -> SimpleException {\n        SimpleException::new_msg(Self::NotImplementedError, msg)\n    }\n\n    /// Creates a ZeroDivisionError for division by zero.\n    ///\n    /// Matches CPython 3.14's format: `ZeroDivisionError('division by zero')`\n    #[must_use]\n    pub(crate) fn zero_division() -> SimpleException {\n        SimpleException::new_msg(Self::ZeroDivisionError, \"division by zero\")\n    }\n\n    /// Creates an OverflowError for string/sequence repetition with count too large.\n    ///\n    /// Matches CPython's format: `OverflowError('cannot fit 'int' into an index-sized integer')`\n    #[must_use]\n    pub(crate) fn overflow_repeat_count() -> SimpleException {\n        SimpleException::new_msg(Self::OverflowError, \"cannot fit 'int' into an index-sized integer\")\n    }\n\n    /// Creates an IndexError for when an integer index is too large to fit in i64.\n    ///\n    /// Matches CPython's format: `IndexError: cannot fit 'int' into an index-sized integer`\n    #[must_use]\n    pub(crate) fn index_error_int_too_large() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"cannot fit 'int' into an index-sized integer\").into()\n    }\n\n    /// Creates an ImportError for when a name cannot be imported from a module.\n    ///\n    /// Matches CPython's format for built-in modules:\n    /// `ImportError: cannot import name 'name' from 'module' (unknown location)`\n    ///\n    /// Sets `hide_caret: true` because CPython doesn't show carets for import errors.\n    #[must_use]\n    pub(crate) fn cannot_import_name(name: &str, module_name: &str) -> RunError {\n        let exc = SimpleException::new_msg(\n            Self::ImportError,\n            format!(\"cannot import name '{name}' from '{module_name}' (unknown location)\"),\n        );\n        RunError::Exc(ExceptionRaise {\n            exc,\n            frame: None,\n            hide_caret: true,\n        })\n    }\n\n    /// Creates a ValueError for negative shift count in bitwise shift operations.\n    ///\n    /// Matches CPython's format: `ValueError: negative shift count`\n    #[must_use]\n    pub(crate) fn value_error_negative_shift_count() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"negative shift count\").into()\n    }\n\n    /// Creates an OverflowError for shift count exceeding integer size.\n    ///\n    /// Matches CPython's format: `OverflowError: Python int too large to convert to C ssize_t`\n    /// Note: CPython uses this message because it tries to convert to ssize_t for the shift amount.\n    #[must_use]\n    pub(crate) fn overflow_shift_count() -> RunError {\n        SimpleException::new_msg(Self::OverflowError, \"Python int too large to convert to C ssize_t\").into()\n    }\n\n    /// Creates a TypeError for unsupported binary operations.\n    ///\n    /// For `+` or `+=` with str/list on the left side, uses CPython's special format:\n    /// `can only concatenate {type} (not \"{other}\") to {type}`\n    ///\n    /// For other cases, uses the generic format:\n    /// `unsupported operand type(s) for {op}: '{left}' and '{right}'`\n    #[must_use]\n    pub(crate) fn binary_type_error(op: &str, lhs_type: Type, rhs_type: Type) -> RunError {\n        let message = if (op == \"+\" || op == \"+=\") && (lhs_type == Type::Str || lhs_type == Type::List) {\n            format!(\"can only concatenate {lhs_type} (not \\\"{rhs_type}\\\") to {lhs_type}\")\n        } else {\n            format!(\"unsupported operand type(s) for {op}: '{lhs_type}' and '{rhs_type}'\")\n        };\n        SimpleException::new_msg(Self::TypeError, message).into()\n    }\n\n    /// Creates a TypeError for unsupported unary operations.\n    ///\n    /// Uses CPython's format: `bad operand type for unary {op}: '{type}'`\n    #[must_use]\n    pub(crate) fn unary_type_error(op: &str, value_type: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"bad operand type for unary {op}: '{value_type}'\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for functions that require an integer argument.\n    ///\n    /// Matches CPython's format: `TypeError: '{type}' object cannot be interpreted as an integer`\n    #[must_use]\n    pub(crate) fn type_error_not_integer(type_: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"'{type_}' object cannot be interpreted as an integer\"),\n        )\n        .into()\n    }\n\n    /// Creates a ZeroDivisionError for zero raised to a negative power.\n    ///\n    /// Matches CPython's format: `ZeroDivisionError: zero to a negative power`\n    /// Note: CPython uses the same message for both int and float zero ** negative.\n    #[must_use]\n    pub(crate) fn zero_negative_power() -> RunError {\n        SimpleException::new_msg(Self::ZeroDivisionError, \"zero to a negative power\").into()\n    }\n\n    /// Creates an OverflowError for exponents that are too large.\n    ///\n    /// Matches CPython's format: `OverflowError: exponent too large`\n    #[must_use]\n    pub(crate) fn overflow_exponent_too_large() -> RunError {\n        SimpleException::new_msg(Self::OverflowError, \"exponent too large\").into()\n    }\n\n    /// Creates a ZeroDivisionError for divmod by zero (both integer and float).\n    ///\n    /// Matches CPython's format: `ZeroDivisionError: division by zero`\n    /// Note: CPython uses the same message for both integer and float divmod.\n    #[must_use]\n    pub(crate) fn divmod_by_zero() -> RunError {\n        SimpleException::new_msg(Self::ZeroDivisionError, \"division by zero\").into()\n    }\n\n    /// Creates a TypeError for str.join() when an item is not a string.\n    ///\n    /// Matches CPython's format: `TypeError: sequence item {index}: expected str instance, {type} found`\n    #[must_use]\n    pub(crate) fn type_error_join_item(index: usize, item_type: Type) -> RunError {\n        SimpleException::new_msg(\n            Self::TypeError,\n            format!(\"sequence item {index}: expected str instance, {item_type} found\"),\n        )\n        .into()\n    }\n\n    /// Creates a TypeError for str.join() when the argument is not iterable.\n    ///\n    /// Matches CPython's format: `TypeError: can only join an iterable`\n    #[must_use]\n    pub(crate) fn type_error_join_not_iterable() -> RunError {\n        SimpleException::new_msg(Self::TypeError, \"can only join an iterable\").into()\n    }\n\n    /// Creates a ValueError for str.index()/str.rindex() when substring is not found.\n    ///\n    /// Matches CPython's format: `ValueError: substring not found`\n    #[must_use]\n    pub(crate) fn value_error_substring_not_found() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"substring not found\").into()\n    }\n\n    /// Creates a ValueError for str.partition()/str.rpartition() with empty separator.\n    ///\n    /// Matches CPython's format: `ValueError: empty separator`\n    #[must_use]\n    pub(crate) fn value_error_empty_separator() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"empty separator\").into()\n    }\n\n    /// Creates a TypeError for fillchar argument that is not a single character.\n    ///\n    /// Matches CPython's format: `TypeError: The fill character must be exactly one character long`\n    #[must_use]\n    pub(crate) fn type_error_fillchar_must_be_single_char() -> RunError {\n        SimpleException::new_msg(Self::TypeError, \"The fill character must be exactly one character long\").into()\n    }\n\n    /// Creates a StopIteration exception for when an iterator is exhausted.\n    ///\n    /// Matches CPython's format: `StopIteration`\n    #[must_use]\n    pub(crate) fn stop_iteration() -> RunError {\n        SimpleException::new_none(Self::StopIteration).into()\n    }\n\n    /// Creates a ValueError for list.index() when item is not found.\n    ///\n    /// Matches CPython's format: `ValueError: list.index(x): x not in list`\n    #[must_use]\n    pub(crate) fn value_error_not_in_list() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"list.index(x): x not in list\").into()\n    }\n\n    /// Creates a ValueError for tuple.index() when item is not found.\n    ///\n    /// Matches CPython's format: `ValueError: tuple.index(x): x not in tuple`\n    #[must_use]\n    pub(crate) fn value_error_not_in_tuple() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"tuple.index(x): x not in tuple\").into()\n    }\n\n    /// Creates a ValueError for list.remove() when item is not found.\n    ///\n    /// Matches CPython's format: `ValueError: list.remove(x): x not in list`\n    #[must_use]\n    pub(crate) fn value_error_remove_not_in_list() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"list.remove(x): x not in list\").into()\n    }\n\n    /// Creates an IndexError for popping from an empty list.\n    ///\n    /// Matches CPython's format: `IndexError: pop from empty list`\n    #[must_use]\n    pub(crate) fn index_error_pop_empty_list() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"pop from empty list\").into()\n    }\n\n    /// Creates an IndexError for list.pop(index) with invalid index.\n    ///\n    /// Matches CPython's format: `IndexError: pop index out of range`\n    #[must_use]\n    pub(crate) fn index_error_pop_out_of_range() -> RunError {\n        SimpleException::new_msg(Self::IndexError, \"pop index out of range\").into()\n    }\n\n    /// Creates a KeyError for popping from an empty dict.\n    ///\n    /// Matches CPython's format: `KeyError: 'popitem(): dictionary is empty'`\n    #[must_use]\n    pub(crate) fn key_error_popitem_empty_dict() -> RunError {\n        SimpleException::new_msg(Self::KeyError, \"'popitem(): dictionary is empty'\").into()\n    }\n\n    /// Creates a LookupError for unknown encoding.\n    ///\n    /// Matches CPython's format: `LookupError: unknown encoding: {encoding}`\n    #[must_use]\n    pub(crate) fn lookup_error_unknown_encoding(encoding: &str) -> RunError {\n        SimpleException::new_msg(Self::LookupError, format!(\"unknown encoding: {encoding}\")).into()\n    }\n\n    /// Creates a UnicodeDecodeError for invalid UTF-8 bytes in decode().\n    ///\n    /// Matches CPython's format: `UnicodeDecodeError: 'utf-8' codec can't decode bytes...`\n    #[must_use]\n    pub(crate) fn unicode_decode_error_invalid_utf8() -> RunError {\n        SimpleException::new_msg(\n            Self::UnicodeDecodeError,\n            \"'utf-8' codec can't decode bytes: invalid utf-8 sequence\",\n        )\n        .into()\n    }\n\n    /// Creates a ValueError for subsequence not found in bytes/str.\n    ///\n    /// Matches CPython's format: `ValueError: subsection not found`\n    #[must_use]\n    pub(crate) fn value_error_subsequence_not_found() -> RunError {\n        SimpleException::new_msg(Self::ValueError, \"subsection not found\").into()\n    }\n\n    /// Creates a LookupError for unknown error handler.\n    ///\n    /// Matches CPython's format: `LookupError: unknown error handler name '{name}'`\n    #[must_use]\n    pub(crate) fn lookup_error_unknown_error_handler(name: &str) -> RunError {\n        SimpleException::new_msg(Self::LookupError, format!(\"unknown error handler name '{name}'\")).into()\n    }\n\n    /// Creates a `re.PatternError` for an invalid regex pattern or unsupported regex feature.\n    ///\n    /// Matches CPython's exception type: `re.PatternError: {message}`\n    #[must_use]\n    pub(crate) fn re_pattern_error(msg: impl fmt::Display) -> RunError {\n        SimpleException::new_msg(Self::RePatternError, msg).into()\n    }\n}\n\n/// Simple lightweight representation of an exception.\n///\n/// This is used for performance reasons for common exception patterns.\n/// Exception messages use `String` for owned storage.\n#[derive(Debug, Clone, PartialEq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct SimpleException {\n    exc_type: ExcType,\n    arg: Option<String>,\n}\n\nimpl fmt::Display for SimpleException {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        self.py_repr_fmt(f)\n    }\n}\nimpl From<MontyException> for SimpleException {\n    fn from(exc: MontyException) -> Self {\n        Self {\n            exc_type: exc.exc_type(),\n            arg: exc.into_message(),\n        }\n    }\n}\n\nimpl SimpleException {\n    /// Creates a new exception with the given type and optional argument message.\n    #[must_use]\n    pub fn new(exc_type: ExcType, arg: Option<String>) -> Self {\n        Self { exc_type, arg }\n    }\n\n    /// Creates a new exception with the given type and argument message.\n    #[must_use]\n    pub fn new_msg(exc_type: ExcType, arg: impl fmt::Display) -> Self {\n        Self {\n            exc_type,\n            arg: Some(arg.to_string()),\n        }\n    }\n\n    /// Creates a new exception with the given type and no argument message.\n    #[must_use]\n    pub fn new_none(exc_type: ExcType) -> Self {\n        Self { exc_type, arg: None }\n    }\n\n    #[must_use]\n    pub fn exc_type(&self) -> ExcType {\n        self.exc_type\n    }\n\n    #[must_use]\n    pub fn arg(&self) -> Option<&String> {\n        self.arg.as_ref()\n    }\n\n    /// str() for an exception\n    #[must_use]\n    pub fn py_str(&self) -> String {\n        match (self.exc_type, &self.arg) {\n            // KeyError expecificaly uses repr of the key for str(exc)\n            (ExcType::KeyError, Some(exc)) => StringRepr(exc).to_string(),\n            (_, Some(arg)) => arg.to_owned(),\n            (_, None) => String::new(),\n        }\n    }\n\n    pub(crate) fn py_type(&self) -> Type {\n        Type::Exception(self.exc_type)\n    }\n\n    /// Returns the exception formatted as Python would repr it.\n    pub fn py_repr_fmt(&self, f: &mut impl Write) -> std::fmt::Result {\n        let type_str: &'static str = self.exc_type.into();\n        write!(f, \"{type_str}(\")?;\n\n        if let Some(arg) = &self.arg {\n            string_repr_fmt(arg, f)?;\n        }\n\n        f.write_char(')')\n    }\n\n    pub(crate) fn with_frame(self, frame: RawStackFrame) -> ExceptionRaise {\n        ExceptionRaise {\n            exc: self,\n            frame: Some(frame),\n            hide_caret: false,\n        }\n    }\n\n    pub(crate) fn with_position(self, position: CodeRange) -> ExceptionRaise {\n        ExceptionRaise {\n            exc: self,\n            frame: Some(RawStackFrame::from_position(position)),\n            hide_caret: false,\n        }\n    }\n\n    /// Gets an attribute from this exception.\n    ///\n    /// Handles the `.args` attribute by allocating a tuple containing the message.\n    /// Returns `Err(AttributeError)` for all other attributes.\n    pub fn py_getattr(\n        &self,\n        attr: &EitherStr,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> RunResult<Option<CallResult>> {\n        // Fast path: interned strings can be matched by ID\n        let is_args = attr\n            .static_string()\n            .map_or_else(|| attr.as_str(interns) == \"args\", |ss| ss == StaticStrings::Args);\n\n        if is_args {\n            // Construct tuple with 0 or 1 elements based on whether arg exists\n            let elements = if let Some(arg_str) = &self.arg {\n                let str_id = heap.allocate(HeapData::Str(Str::from(arg_str.clone())))?;\n                smallvec![Value::Ref(str_id)]\n            } else {\n                smallvec![]\n            };\n            Ok(Some(CallResult::Value(allocate_tuple(elements, heap)?)))\n        } else {\n            Ok(None)\n        }\n    }\n}\n\n/// A raised exception with optional stack frame for traceback.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct ExceptionRaise {\n    pub exc: SimpleException,\n    /// The stack frame where the exception was raised (first in vec is closest \"bottom\" frame).\n    pub frame: Option<RawStackFrame>,\n    /// Whether to hide the caret marker when creating the stack frame.\n    ///\n    /// CPython doesn't show carets for attribute GET errors, but does show them\n    /// for attribute SET errors. This flag allows error creators to specify\n    /// whether the caret should be hidden.\n    #[serde(default)]\n    pub hide_caret: bool,\n}\n\nimpl From<SimpleException> for ExceptionRaise {\n    fn from(exc: SimpleException) -> Self {\n        Self {\n            exc,\n            frame: None,\n            hide_caret: false,\n        }\n    }\n}\n\nimpl From<MontyException> for ExceptionRaise {\n    fn from(exc: MontyException) -> Self {\n        Self {\n            exc: exc.into(),\n            frame: None,\n            hide_caret: false,\n        }\n    }\n}\n\nimpl ExceptionRaise {\n    /// Adds a caller's frame as the outermost frame in the traceback chain.\n    ///\n    /// This is used when an exception propagates up through call frames.\n    /// The new frame becomes the ultimate parent (displayed first in traceback,\n    /// since tracebacks show \"most recent call last\").\n    ///\n    /// Special case: If the innermost frame has no name yet (created with `with_position`),\n    /// this sets its name instead of creating a new parent. This happens when the error\n    /// is raised from a namespace lookup - the initial frame has the position but not\n    /// the function name, which gets filled in as the error propagates.\n    pub(crate) fn add_caller_frame(&mut self, position: CodeRange, name: StringId) {\n        self.add_caller_frame_inner(position, name, false);\n    }\n\n    fn add_caller_frame_inner(&mut self, position: CodeRange, name: StringId, hide_caret: bool) {\n        if let Some(ref mut frame) = self.frame {\n            // If innermost frame has no name, set it instead of adding a parent\n            // This handles errors from namespace lookups which create nameless frames\n            if frame.frame_name.is_none() {\n                frame.frame_name = Some(name);\n                frame.hide_caret = hide_caret;\n                return;\n            }\n            // Find the outermost frame (the one with no parent) and add the new frame as its parent\n            let mut current = frame;\n            while current.parent.is_some() {\n                current = current.parent.as_mut().unwrap();\n            }\n            let mut new_frame = RawStackFrame::new(position, name, None);\n            new_frame.hide_caret = hide_caret;\n            current.parent = Some(Box::new(new_frame));\n        } else {\n            // No frame yet - create one\n            let mut new_frame = RawStackFrame::new(position, name, None);\n            new_frame.hide_caret = hide_caret;\n            self.frame = Some(new_frame);\n        }\n    }\n\n    /// Converts this exception to a `MontyException` for the public API.\n    ///\n    /// Uses `Interns` to resolve `StringId` references to actual strings.\n    /// Extracts preview lines from the source code for traceback display.\n    #[must_use]\n    pub fn into_python_exception(self, interns: &Interns, source: &str) -> MontyException {\n        let traceback = self\n            .frame\n            .map(|frame| {\n                let mut frames = Vec::new();\n                let mut current = Some(&frame);\n                while let Some(f) = current {\n                    frames.push(StackFrame::from_raw(f, interns, source));\n                    current = f.parent.as_deref();\n                }\n                // Reverse so outermost frame is first (Python's \"most recent call last\" ordering)\n                frames.reverse();\n                frames\n            })\n            .unwrap_or_default();\n\n        MontyException::new_full(self.exc.exc_type(), self.exc.arg().cloned(), traceback)\n    }\n}\n\n/// A stack frame for traceback information.\n///\n/// Stores position information and optional function name as StringId.\n/// The actual name string must be looked up externally when formatting the traceback.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct RawStackFrame {\n    pub position: CodeRange,\n    /// The name of the frame (function name StringId, or None for module-level code).\n    pub frame_name: Option<StringId>,\n    pub parent: Option<Box<Self>>,\n    /// Whether to hide the caret marker in the traceback for this frame.\n    ///\n    /// Set to `true` for:\n    /// - `raise` statements (CPython doesn't show carets for raise)\n    /// - `AttributeError` on attribute access (CPython doesn't show carets for these)\n    pub hide_caret: bool,\n}\n\nimpl RawStackFrame {\n    pub(crate) fn new(position: CodeRange, frame_name: StringId, parent: Option<&Self>) -> Self {\n        Self {\n            position,\n            frame_name: Some(frame_name),\n            parent: parent.map(|p| Box::new(p.clone())),\n            hide_caret: false,\n        }\n    }\n\n    fn from_position(position: CodeRange) -> Self {\n        Self {\n            position,\n            frame_name: None,\n            parent: None,\n            hide_caret: false,\n        }\n    }\n\n    /// Creates a new frame for a raise statement (no caret will be shown).\n    pub(crate) fn from_raise(position: CodeRange, frame_name: StringId) -> Self {\n        Self {\n            position,\n            frame_name: Some(frame_name),\n            parent: None,\n            hide_caret: true,\n        }\n    }\n}\n\n/// Runtime error types that can occur during execution.\n///\n/// Three variants:\n/// - `Internal`: Bug in interpreter implementation (static message)\n/// - `Exc`: Python exception that can be caught by try/except (when implemented)\n/// - `UncatchableExc`: Python exception from resource limits that CANNOT be caught\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum RunError {\n    /// Internal interpreter error - indicates a bug in Monty, not user code.\n    Internal(Cow<'static, str>),\n    /// Catchable Python exception (e.g., ValueError, TypeError).\n    Exc(ExceptionRaise),\n    /// Uncatchable Python exception from resource limits (MemoryError, TimeoutError).\n    ///\n    /// These exceptions display with proper tracebacks like normal Python exceptions,\n    /// but cannot be caught by try/except blocks. This prevents untrusted code from\n    /// suppressing resource limit violations.\n    UncatchableExc(ExceptionRaise),\n}\n\nimpl From<ExceptionRaise> for RunError {\n    fn from(exc: ExceptionRaise) -> Self {\n        Self::Exc(exc)\n    }\n}\n\nimpl From<SimpleException> for RunError {\n    fn from(exc: SimpleException) -> Self {\n        Self::Exc(exc.into())\n    }\n}\n\nimpl From<MontyException> for RunError {\n    fn from(exc: MontyException) -> Self {\n        Self::Exc(exc.into())\n    }\n}\n\nimpl From<FormatError> for RunError {\n    fn from(err: FormatError) -> Self {\n        let exc_type = match &err {\n            FormatError::Overflow(_) => ExcType::OverflowError,\n            FormatError::InvalidAlignment(_) | FormatError::ValueError(_) => ExcType::ValueError,\n        };\n        Self::Exc(SimpleException::new_msg(exc_type, err).into())\n    }\n}\n\nimpl RunError {\n    /// Converts this runtime error to a `MontyException` for the public API.\n    ///\n    /// Internal errors are converted to `RuntimeError` exceptions with no traceback.\n    #[must_use]\n    pub fn into_python_exception(self, interns: &Interns, source: &str) -> MontyException {\n        match self {\n            Self::Exc(exc) | Self::UncatchableExc(exc) => exc.into_python_exception(interns, source),\n            Self::Internal(err) => MontyException::runtime_error(format!(\"Internal error in monty: {err}\")),\n        }\n    }\n\n    pub fn internal(msg: impl Into<Cow<'static, str>>) -> Self {\n        Self::Internal(msg.into())\n    }\n}\n\n/// Formats a list of parameter names for error messages.\n///\n/// Examples:\n/// - `[\"a\"]` -> `'a'`\n/// - `[\"a\", \"b\"]` -> `'a' and 'b'`\n/// - `[\"a\", \"b\", \"c\"]` -> `'a', 'b' and 'c'`\nfn format_param_names(names: &[&str]) -> String {\n    match names.len() {\n        0 => String::new(),\n        1 => format!(\"'{}'\", names[0]),\n        2 => format!(\"'{}' and '{}'\", names[0], names[1]),\n        _ => {\n            let last = names.last().unwrap();\n            let rest: Vec<_> = names[..names.len() - 1].iter().map(|n| format!(\"'{n}'\")).collect();\n            format!(\"{} and '{last}'\", rest.join(\", \"))\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/exception_public.rs",
    "content": "use std::fmt::{self, Write};\n\nuse crate::{\n    exception_private::{ExcType, RawStackFrame},\n    intern::Interns,\n    parse::CodeRange,\n    types::str::StringRepr,\n};\n\n/// Public representation of a Monty exception.\n#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct MontyException {\n    /// The exception type raised\n    exc_type: ExcType,\n    /// Optional exception message explaining what went wrong\n    message: Option<String>,\n    /// Stack trace of the exception, first is the outermost frame shown first in the traceback\n    traceback: Vec<StackFrame>,\n}\n\n/// Number of identical consecutive frames to show before collapsing.\n///\n/// CPython shows 3 identical frames, then \"[Previous line repeated N more times]\".\nconst REPEAT_FRAMES_SHOWN: usize = 3;\n\n/// Display implementation for MontyException should exactly match python traceback format.\nimpl fmt::Display for MontyException {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        // Print the traceback header if we have frames\n        if !self.traceback.is_empty() {\n            writeln!(f, \"Traceback (most recent call last):\")?;\n        }\n\n        // Print frames, collapsing consecutive identical frames like CPython does\n        let mut i = 0;\n        while i < self.traceback.len() {\n            let frame = &self.traceback[i];\n\n            // Count consecutive identical frames\n            let mut repeat_count = 1;\n            while i + repeat_count < self.traceback.len()\n                && frames_are_identical(frame, &self.traceback[i + repeat_count])\n            {\n                repeat_count += 1;\n            }\n\n            if repeat_count > REPEAT_FRAMES_SHOWN {\n                // Show first REPEAT_FRAMES_SHOWN frames, then collapse the rest\n                for j in 0..REPEAT_FRAMES_SHOWN {\n                    write!(f, \"{}\", &self.traceback[i + j])?;\n                }\n                let collapsed = repeat_count - REPEAT_FRAMES_SHOWN;\n                writeln!(f, \"  [Previous line repeated {collapsed} more times]\")?;\n                i += repeat_count;\n            } else {\n                // Show all frames in this group\n                for j in 0..repeat_count {\n                    write!(f, \"{}\", &self.traceback[i + j])?;\n                }\n                i += repeat_count;\n            }\n        }\n\n        if let Some(msg) = &self.message {\n            write!(f, \"{}: {}\", self.exc_type, msg)\n        } else {\n            write!(f, \"{}\", self.exc_type)\n        }\n    }\n}\n\nimpl std::error::Error for MontyException {}\n\nimpl MontyException {\n    /// Create a new MontyException with the given exception type and message.\n    ///\n    /// You can't provide a traceback here, it's send when raising the exception.\n    #[must_use]\n    pub fn new(exc_type: ExcType, message: Option<String>) -> Self {\n        Self {\n            exc_type,\n            message,\n            traceback: vec![],\n        }\n    }\n\n    /// The exception type raised.\n    #[must_use]\n    pub fn exc_type(&self) -> ExcType {\n        self.exc_type\n    }\n\n    /// Optional exception message explaining what went wrong.\n    ///\n    /// Equivalent of python's `exc.args[0]`\n    #[must_use]\n    pub fn message(&self) -> Option<&str> {\n        self.message.as_deref()\n    }\n\n    /// Optional exception message explaining what went wrong.\n    ///\n    /// This takes ownership of the MontyException and returns an owned String.\n    ///\n    /// Equivalent of python's `exc.args[0]`\n    #[must_use]\n    pub fn into_message(self) -> Option<String> {\n        self.message\n    }\n\n    /// Stack trace of the exception, first is the outermost frame shown first in the traceback\n    #[must_use]\n    pub fn traceback(&self) -> &[StackFrame] {\n        &self.traceback\n    }\n\n    /// Returns a compact summary of the exception.\n    ///\n    /// Format: `ExceptionType: message` (e.g., `NotImplementedError: feature not supported`)\n    /// If there's no message, just returns the exception type name.\n    #[must_use]\n    pub fn summary(&self) -> String {\n        if let Some(msg) = &self.message {\n            format!(\"{}: {}\", self.exc_type, msg)\n        } else {\n            self.exc_type.to_string()\n        }\n    }\n\n    /// Returns the exception formatted as Python's repr() would display it.\n    ///\n    /// Format: `ExceptionType('message')` (e.g., `ValueError('invalid value')`)\n    /// Uses appropriate quoting for messages containing quotes.\n    #[must_use]\n    pub fn py_repr(&self) -> String {\n        let type_str: &'static str = self.exc_type.into();\n        if let Some(msg) = &self.message {\n            format!(\"{}({})\", type_str, StringRepr(msg))\n        } else {\n            format!(\"{type_str}()\")\n        }\n    }\n\n    pub(crate) fn new_full(exc_type: ExcType, message: Option<String>, traceback: Vec<StackFrame>) -> Self {\n        Self {\n            exc_type,\n            message,\n            traceback,\n        }\n    }\n\n    pub(crate) fn runtime_error(err: impl fmt::Display) -> Self {\n        Self {\n            exc_type: ExcType::RuntimeError,\n            message: Some(err.to_string()),\n            traceback: vec![],\n        }\n    }\n}\n\n/// Check if two stack frames are identical for the purpose of collapsing repeated frames.\n///\n/// Two frames are identical if they have the same filename, line number, and function name.\nfn frames_are_identical(a: &StackFrame, b: &StackFrame) -> bool {\n    a.filename == b.filename && a.start.line == b.start.line && a.frame_name == b.frame_name\n}\n\n/// A single frame in a Python traceback.\n///\n/// Contains all the information needed to display a traceback line:\n/// the file location, function name, and optional source code preview.\n///\n/// # Caret Markers\n///\n/// Monty uses only `~` characters for caret markers in tracebacks, unlike CPython 3.11+\n/// which uses `~` for the function name and `^` for arguments (e.g., `~~~~~~~~~~~^^^^^^^^^^^`).\n/// This simplification is intentional - Monty marks the entire expression span uniformly.\n#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct StackFrame {\n    /// The filename where the code is located.\n    pub filename: String,\n    /// Start position in the source code.\n    pub start: CodeLoc,\n    /// End position in the source code.\n    pub end: CodeLoc,\n    /// The name of the frame (function name, or None for module-level code).\n    pub frame_name: Option<String>,\n    /// The source code line for preview in the traceback.\n    pub preview_line: Option<String>,\n    /// Whether to hide the caret marker in the traceback for this frame.\n    ///\n    /// Set to `true` for:\n    /// - `raise` statements (CPython doesn't show carets for raise)\n    /// - `AttributeError` on attribute access (CPython doesn't show carets for these)\n    pub hide_caret: bool,\n    /// Whether to hide the `, in <name>` part of the frame line.\n    ///\n    /// Set to `true` for `SyntaxError` where CPython doesn't show the frame name.\n    /// CPython's SyntaxError format: `  File \"...\", line N`\n    /// vs runtime error format: `  File \"...\", line N, in <module>`\n    pub hide_frame_name: bool,\n}\n\nimpl fmt::Display for StackFrame {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        // SyntaxError format: `  File \"...\", line N`\n        // Runtime error format: `  File \"...\", line N, in <module>`\n        if self.hide_frame_name {\n            write!(f, r#\"  File \"{}\", line {}\"#, self.filename, self.start.line)?;\n        } else {\n            write!(f, r#\"  File \"{}\", line {}, in \"#, self.filename, self.start.line)?;\n            if let Some(frame_name) = &self.frame_name {\n                f.write_str(frame_name)?;\n            } else {\n                f.write_str(\"<module>\")?;\n            }\n        }\n\n        if let Some(line) = &self.preview_line {\n            // Strip leading whitespace like CPython does\n            let trimmed = line.trim_start();\n            writeln!(f, \"\\n    {trimmed}\")?;\n\n            // Hide caret for raise statements, AttributeError, etc.\n            if !self.hide_caret {\n                let leading_spaces = line.len() - trimmed.len();\n                // Calculate caret position relative to the trimmed line\n                // Column is 1-indexed, so subtract 1, then subtract leading spaces we stripped\n                let caret_start = if self.start.column as usize > leading_spaces {\n                    4 + self.start.column as usize - leading_spaces - 1\n                } else {\n                    4\n                };\n                f.write_str(&\" \".repeat(caret_start))?;\n                writeln!(f, \"{}\", \"~\".repeat((self.end.column - self.start.column) as usize))?;\n            }\n        } else {\n            f.write_char('\\n')?;\n        }\n        Ok(())\n    }\n}\n\nimpl StackFrame {\n    pub(crate) fn from_raw(f: &RawStackFrame, interns: &Interns, source: &str) -> Self {\n        let filename = interns.get_str(f.position.filename).to_string();\n        Self {\n            filename,\n            start: f.position.start(),\n            end: f.position.end(),\n            frame_name: f.frame_name.map(|id| interns.get_str(id).to_string()),\n            preview_line: f\n                .position\n                .preview_line_number()\n                .and_then(|ln| source.lines().nth(ln as usize))\n                .map(str::to_string),\n            hide_caret: f.hide_caret,\n            hide_frame_name: false,\n        }\n    }\n\n    /// Creates a `StackFrame` from a `CodeRange` for SyntaxError.\n    ///\n    /// Sets `hide_frame_name: true` because CPython's SyntaxError format doesn't\n    /// show the `, in <module>` part.\n    pub(crate) fn from_position_syntax_error(position: CodeRange, filename: &str, source: &str) -> Self {\n        Self {\n            filename: filename.to_string(),\n            start: position.start(),\n            end: position.end(),\n            frame_name: None,\n            preview_line: position\n                .preview_line_number()\n                .and_then(|ln| source.lines().nth(ln as usize))\n                .map(str::to_string),\n            hide_caret: false,\n            hide_frame_name: true,\n        }\n    }\n\n    pub(crate) fn from_position(position: CodeRange, filename: &str, source: &str) -> Self {\n        Self {\n            filename: filename.to_string(),\n            start: position.start(),\n            end: position.end(),\n            frame_name: None,\n            preview_line: position\n                .preview_line_number()\n                .and_then(|ln| source.lines().nth(ln as usize))\n                .map(str::to_string),\n            hide_caret: false,\n            hide_frame_name: false,\n        }\n    }\n\n    /// Creates a `StackFrame` from a `CodeRange` without caret markers.\n    ///\n    /// Used for errors like `ImportError` where CPython doesn't show caret markers.\n    pub(crate) fn from_position_no_caret(position: CodeRange, filename: &str, source: &str) -> Self {\n        Self {\n            filename: filename.to_string(),\n            start: position.start(),\n            end: position.end(),\n            frame_name: None,\n            preview_line: position\n                .preview_line_number()\n                .and_then(|ln| source.lines().nth(ln as usize))\n                .map(str::to_string),\n            hide_caret: true,\n            hide_frame_name: false,\n        }\n    }\n}\n\n/// A line and column position in source code.\n///\n/// Uses 1-based indexing for both line and column to match Python's conventions.\n#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]\npub struct CodeLoc {\n    /// Line number (1-based).\n    pub line: u16,\n    /// Column number (1-based).\n    pub column: u16,\n}\n\nimpl Default for CodeLoc {\n    fn default() -> Self {\n        Self { line: 1, column: 1 }\n    }\n}\n\nimpl CodeLoc {\n    /// Creates a new CodeLoc from usize values.\n    ///\n    /// Lines and columns numbers are 1-indexed for display, hence `+1`\n    ///\n    /// # Panics\n    /// Panics if the line or column number overflows `u16`.\n    #[must_use]\n    pub fn new(line: usize, column: usize) -> Self {\n        Self {\n            line: u16::try_from(line).expect(\"Line number overflow\") + 1,\n            column: u16::try_from(column).expect(\"Column number overflow\") + 1,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/expressions.rs",
    "content": "use crate::{\n    args::ArgExprs,\n    builtins::Builtins,\n    fstring::FStringPart,\n    intern::{BytesId, LongIntId, StringId},\n    namespace::NamespaceId,\n    parse::{CodeRange, ParsedSignature, Try},\n    signature::Signature,\n    value::{EitherStr, Marker, Value},\n};\n\n/// Indicates which namespace a variable reference belongs to.\n///\n/// This is determined at prepare time based on Python's scoping rules:\n/// - Variables assigned in a function are Local (unless declared `global`)\n/// - Variables only read (not assigned) that exist at module level are Global\n/// - The `global` keyword explicitly marks a variable as Global\n/// - Variables declared `nonlocal` or implicitly captured from enclosing scopes\n///   are accessed through Cells\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]\npub enum NameScope {\n    /// Variable is in the current frame's local namespace (assigned somewhere in this function).\n    ///\n    /// If accessed before assignment, raises `UnboundLocalError`.\n    #[default]\n    Local,\n    /// Variable reference that doesn't exist in any scope.\n    ///\n    /// A local slot is allocated but never assigned. Accessing raises `NameError`\n    /// (not `UnboundLocalError`) because the name was never defined anywhere.\n    LocalUnassigned,\n    /// Variable is in the module-level global namespace\n    Global,\n    /// Variable accessed through a cell (heap-allocated container).\n    ///\n    /// Used for both:\n    /// - Variables captured from enclosing scopes (free variables)\n    /// - Variables in this function that are captured by nested functions (cell variables)\n    ///\n    /// The namespace slot contains `Value::Ref(cell_id)` pointing to a `HeapData::Cell`.\n    /// Access requires dereferencing through the cell.\n    Cell,\n}\n\n/// An identifier (variable or function name) with source location and scope information.\n///\n/// The name is stored as a `StringId` which indexes into the string interner.\n/// To get the actual string, look it up in the `Interns` storage.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub struct Identifier {\n    pub position: CodeRange,\n    /// Interned name ID - look up in Interns to get the actual string.\n    pub name_id: StringId,\n    opt_namespace_id: Option<NamespaceId>,\n    /// Which namespace this identifier refers to (determined at prepare time)\n    pub scope: NameScope,\n}\n\nimpl Identifier {\n    /// Creates a new identifier with unknown scope (to be resolved during prepare phase).\n    pub fn new(name_id: StringId, position: CodeRange) -> Self {\n        Self {\n            name_id,\n            position,\n            opt_namespace_id: None,\n            scope: NameScope::Local,\n        }\n    }\n\n    /// Creates a new identifier with resolved namespace index and explicit scope.\n    pub fn new_with_scope(name_id: StringId, position: CodeRange, namespace_id: NamespaceId, scope: NameScope) -> Self {\n        Self {\n            name_id,\n            position,\n            opt_namespace_id: Some(namespace_id),\n            scope,\n        }\n    }\n\n    pub fn namespace_id(&self) -> NamespaceId {\n        self.opt_namespace_id\n            .expect(\"Identifier not prepared with namespace_id\")\n    }\n}\n\n/// Target of a function call expression.\n///\n/// Represents a callable that can be either:\n/// - A builtin function or exception resolved at parse time (`print`, `len`, `ValueError`, etc.)\n/// - A name that will be looked up in the namespace at runtime (for callable variables)\n///\n/// Separate from Value to allow deriving Clone without Value's Clone restrictions.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub enum Callable {\n    /// A builtin function like `print`, `len`, `str`, etc.\n    Builtin(Builtins),\n    /// A name to be looked up in the namespace at runtime (e.g., `x` in `x = len; x('abc')`).\n    Name(Identifier),\n}\n\n/// An item in a list, tuple, or set literal.\n///\n/// PEP 448 allows any number of `*expr` unpack items to appear alongside\n/// regular values in list/tuple/set literals (e.g., `[1, *a, 2]`).\n/// This enum represents either a plain value or an iterable to be unpacked.\n///\n/// Used in `Expr::List`, `Expr::Tuple`, and `Expr::Set` to represent each\n/// element of the literal. When the fast path is taken (no unpack items),\n/// only `Value` variants are present and the compiler emits a single\n/// `BuildList`/`BuildTuple`/`BuildSet` instruction. When any `Unpack` item\n/// is present, the compiler emits `Build*(0)` followed by per-item\n/// `ListAppend`/`SetAdd` and `ListExtend`/`SetExtend` instructions.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) enum SequenceItem {\n    /// A plain expression value in the literal.\n    Value(ExprLoc),\n    /// An `*expr` unpack — the iterable is expanded in-place.\n    Unpack(ExprLoc),\n}\n\n/// An item in a dict literal.\n///\n/// PEP 448 allows `**expr` unpack items to appear alongside normal key:value\n/// pairs in dict literals (e.g., `{'a': 1, **d, 'b': 2}`). Duplicate keys\n/// from later items silently overwrite earlier ones (unlike `**kwargs` in\n/// function calls, where duplicates raise `TypeError`).\n///\n/// Used in `Expr::Dict`. When no `Unpack` items are present the compiler\n/// emits a single `BuildDict` instruction. Otherwise it emits `BuildDict(0)`\n/// followed by per-item `DictSetItem` and `DictUpdate` instructions.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) enum DictItem {\n    /// A plain `key: value` pair.\n    Pair(ExprLoc, ExprLoc),\n    /// A `**expr` unpack — the mapping is merged in-place, later keys win.\n    Unpack(ExprLoc),\n}\n\n/// An expression in the AST.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum Expr {\n    Literal(Literal),\n    Builtin(Builtins),\n    Name(Identifier),\n    /// Function call expression.\n    ///\n    /// The `callable` can be a Builtin, ExcType (resolved at parse time), or a Name\n    /// that will be looked up in the namespace at runtime.\n    Call {\n        callable: Callable,\n        /// ArgExprs is relatively large and would require Box anyway since it uses ExprLoc, so keep Expr small\n        /// by using a box here\n        args: Box<ArgExprs>,\n    },\n    /// Method call on an object (e.g., `obj.method(args)`).\n    ///\n    /// The object expression is evaluated first, then the method is looked up\n    /// and called with the given arguments. Supports chained attribute access\n    /// like `a.b.c.method()`.\n    AttrCall {\n        object: Box<ExprLoc>,\n        attr: EitherStr,\n        /// same as above for Box\n        args: Box<ArgExprs>,\n    },\n    /// Expression call (e.g., `(lambda x: x + 1)(5)` or `get_func()(args)`).\n    ///\n    /// Calls an arbitrary expression as a callable. The callable expression\n    /// is evaluated first, then called with the given arguments.\n    IndirectCall {\n        /// The expression that evaluates to a callable.\n        callable: Box<ExprLoc>,\n        args: Box<ArgExprs>,\n    },\n    /// Attribute access expression (e.g., `point.x` or `a.b.c`).\n    ///\n    /// Retrieves the value of an attribute from an object. For dataclasses,\n    /// this returns the field value. For other types, this may trigger\n    /// special attribute handling. Supports chained attribute access.\n    AttrGet {\n        object: Box<ExprLoc>,\n        attr: EitherStr,\n    },\n    Op {\n        left: Box<ExprLoc>,\n        op: Operator,\n        right: Box<ExprLoc>,\n    },\n    CmpOp {\n        left: Box<ExprLoc>,\n        op: CmpOperator,\n        right: Box<ExprLoc>,\n    },\n    /// Chain comparison expression: `a < b < c < d`\n    ///\n    /// Unlike single comparisons, chain comparisons evaluate intermediate values\n    /// only once and short-circuit on the first false result. Compiled to bytecode\n    /// that uses stack manipulation (Dup, Rot) rather than temporary variables,\n    /// avoiding namespace pollution.\n    ChainCmp {\n        /// The leftmost operand in the chain.\n        left: Box<ExprLoc>,\n        /// Sequence of (operator, operand) pairs: `[(op1, b), (op2, c), ...]`\n        comparisons: Vec<(CmpOperator, ExprLoc)>,\n    },\n    /// List literal: `[a, *b, c]`\n    ///\n    /// Each element is a `SequenceItem` which may be a plain value or an `*unpack`.\n    /// When no unpack items are present (common case), the compiler emits a single\n    /// `BuildList(N)`. When any unpack is present it emits `BuildList(0)` followed\n    /// by per-item `ListAppend`/`ListExtend` instructions.\n    List(Vec<SequenceItem>),\n    /// Tuple literal: `(a, *b, c)` or `a, *b, c`\n    ///\n    /// Same compilation strategy as `List` but ends with `ListToTuple`.\n    Tuple(Vec<SequenceItem>),\n    Subscript {\n        object: Box<ExprLoc>,\n        index: Box<ExprLoc>,\n    },\n    /// Slice literal expression from `x[start:stop:step]` syntax.\n    ///\n    /// Each component is optional (None means use the default for that position).\n    /// This expression creates a `slice` object when evaluated.\n    Slice {\n        lower: Option<Box<ExprLoc>>,\n        upper: Option<Box<ExprLoc>>,\n        step: Option<Box<ExprLoc>>,\n    },\n    /// Dict literal: `{'a': 1, **d, 'b': 2}`\n    ///\n    /// Each element is a `DictItem` which may be a plain `key: value` pair or a `**unpack`.\n    /// When no unpack items are present the compiler emits `BuildDict(N)`. Otherwise it\n    /// emits `BuildDict(0)` followed by per-item `DictSetItem`/`DictUpdate` instructions.\n    /// Duplicate keys from later items silently overwrite earlier ones.\n    Dict(Vec<DictItem>),\n    /// Set literal expression: `{1, *a, 2}`.\n    ///\n    /// Note: `{}` is always a dict, not an empty set. Use `set()` for empty sets.\n    /// Compilation strategy mirrors `List` but uses `SetAdd`/`SetExtend`.\n    Set(Vec<SequenceItem>),\n    /// Unary `not` expression - evaluates to the boolean negation of the operand's truthiness.\n    Not(Box<ExprLoc>),\n    /// Unary minus expression - negates a numeric value.\n    UnaryMinus(Box<ExprLoc>),\n    /// Unary plus expression - returns value as-is for numbers, converts bools to int.\n    UnaryPlus(Box<ExprLoc>),\n    /// Unary bitwise NOT expression - inverts all bits of an integer.\n    UnaryInvert(Box<ExprLoc>),\n    /// Await expression - suspends execution until the awaited value resolves.\n    ///\n    /// Can await `ExternalFuture`, `Coroutine`, or `GatherFuture` values.\n    /// Raises `TypeError` for non-awaitable values.\n    /// Unlike standard Python, `await` is allowed at module level (like Jupyter notebooks).\n    Await(Box<ExprLoc>),\n    /// F-string expression containing literal and interpolated parts.\n    ///\n    /// At evaluation time, each part is processed in sequence:\n    /// - Literal parts are used directly\n    /// - Interpolation parts have their expression evaluated, converted, and formatted\n    ///\n    /// The results are concatenated to produce the final string.\n    FString(Vec<FStringPart>),\n    /// Conditional expression (ternary operator): `body if test else orelse`\n    ///\n    /// Only one of body/orelse is evaluated based on the truthiness of test.\n    /// This implements short-circuit evaluation - the branch not taken is never executed.\n    IfElse {\n        test: Box<ExprLoc>,\n        body: Box<ExprLoc>,\n        orelse: Box<ExprLoc>,\n    },\n    /// List comprehension: `[elt for target in iter if cond...]`\n    ///\n    /// Builds a new list by iterating and optionally filtering. Loop variables\n    /// are scoped to the comprehension and do not leak to the enclosing scope.\n    ListComp {\n        elt: Box<ExprLoc>,\n        generators: Vec<Comprehension>,\n    },\n    /// Set comprehension: `{elt for target in iter if cond...}`\n    ///\n    /// Builds a new set by iterating and optionally filtering. Duplicate values\n    /// are deduplicated. Loop variables are scoped to the comprehension.\n    SetComp {\n        elt: Box<ExprLoc>,\n        generators: Vec<Comprehension>,\n    },\n    /// Dict comprehension: `{key: value for target in iter if cond...}`\n    ///\n    /// Builds a new dict by iterating and optionally filtering. Later values\n    /// overwrite earlier ones for duplicate keys. Loop variables are scoped\n    /// to the comprehension.\n    DictComp {\n        key: Box<ExprLoc>,\n        value: Box<ExprLoc>,\n        generators: Vec<Comprehension>,\n    },\n    /// Raw lambda expression from the parser, before preparation.\n    ///\n    /// This variant is produced during parsing and contains unprepared data.\n    /// During the prepare phase, it gets converted to `Expr::Lambda` with a\n    /// fully prepared `PreparedFunctionDef`.\n    LambdaRaw {\n        /// The interned `<lambda>` name ID.\n        name_id: StringId,\n        /// The parsed lambda signature (parameters and defaults).\n        signature: ParsedSignature,\n        /// The lambda body expression (not yet prepared).\n        body: Box<ExprLoc>,\n    },\n    /// Lambda expression: `lambda args: body` (prepared form).\n    ///\n    /// A lambda is an anonymous function that returns a single expression.\n    /// It's compiled identically to a regular function, but with the name `<lambda>`\n    /// and an implicit return of the body expression. The resulting function value\n    /// stays on the stack as an expression result (not stored to a name).\n    Lambda {\n        /// The prepared function definition containing signature, body, and closure info.\n        /// The body is wrapped as `[Node::Return(body_expr)]` during preparation.\n        func_def: Box<PreparedFunctionDef>,\n    },\n    /// Named expression (walrus operator): `(target := value)`\n    ///\n    /// Evaluates `value`, assigns it to `target`, and returns the value as the\n    /// expression result. The target is treated as an assignment for scope analysis,\n    /// so it creates a local binding in the enclosing scope.\n    ///\n    /// Per PEP 572, in comprehensions the target binds in the enclosing scope,\n    /// not the comprehension's implicit scope.\n    Named {\n        target: Identifier,\n        value: Box<ExprLoc>,\n    },\n}\n\n/// Target for tuple unpacking - can be a single name, nested tuple, or starred target.\n///\n/// Supports recursive structures like `(a, b), c` or `a, (b, c)`.\n/// Also supports starred targets like `first, *rest = [1, 2, 3, 4]`.\n/// Used in assignment statements, for loop targets, and comprehension targets.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum UnpackTarget {\n    /// Single identifier: `a`\n    Name(Identifier),\n    /// Nested tuple: `(a, b)` - can contain further nested tuples\n    Tuple {\n        /// The targets to unpack into (can be names or nested tuples)\n        targets: Vec<Self>,\n        /// Source position covering all targets (for error caret placement)\n        position: CodeRange,\n    },\n    /// Starred target: `*rest` - captures remaining values into a list.\n    ///\n    /// Only one starred target is allowed per unpacking level.\n    Starred(Identifier),\n}\n\n/// A generator clause in a comprehension: `for target in iter [if cond1] [if cond2]...`\n///\n/// Represents one `for` clause with zero or more `if` filters. Multiple generators\n/// create nested iteration (the rightmost varies fastest).\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct Comprehension {\n    /// Loop variable - either single identifier or tuple unpacking pattern.\n    pub target: UnpackTarget,\n    /// Iterable expression to loop over.\n    pub iter: ExprLoc,\n    /// Zero or more filter conditions (all must be truthy for the element to be included).\n    pub ifs: Vec<ExprLoc>,\n}\n\nimpl Expr {\n    pub fn is_none(&self) -> bool {\n        matches!(self, Self::Literal(Literal::None))\n    }\n}\n\n/// Represents values that can be produced purely from the parser/prepare pipeline.\n///\n/// Const values are intentionally detached from the runtime heap so we can keep\n/// parse-time transformations (constant folding, namespace seeding, etc.) free from\n/// reference-count semantics. Only once execution begins are these literals turned\n/// into real `Value`s that participate in the interpreter's runtime rules.\n///\n/// Note: unlike the AST `Constant` type, we store tuples only as expressions since they\n/// can't always be recorded as constants.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub enum Literal {\n    Ellipsis,\n    None,\n    Bool(bool),\n    Int(i64),\n    Float(f64),\n    /// An interned string literal. The StringId references the string in the Interns table.\n    Str(StringId),\n    /// An interned bytes literal. The BytesId references the bytes in the Interns table.\n    Bytes(BytesId),\n    /// An interned long integer literal. The `LongIntId` references the value in the Interns table.\n    /// Used for integer literals that exceed the i64 range.\n    LongInt(LongIntId),\n    /// A marker value (e.g., typing constructs like Any, Optional, etc.).\n    Marker(Marker),\n}\n\nimpl From<Literal> for Value {\n    /// Converts the literal into its runtime `Value` counterpart.\n    ///\n    /// This is the only place parse-time data crosses the boundary into runtime\n    /// semantics, ensuring every literal follows the same conversion path.\n    fn from(literal: Literal) -> Self {\n        match literal {\n            Literal::Ellipsis => Self::Ellipsis,\n            Literal::None => Self::None,\n            Literal::Bool(b) => Self::Bool(b),\n            Literal::Int(v) => Self::Int(v),\n            Literal::Float(v) => Self::Float(v),\n            Literal::Str(string_id) => Self::InternString(string_id),\n            Literal::Bytes(bytes_id) => Self::InternBytes(bytes_id),\n            Literal::LongInt(long_int_id) => Self::InternLongInt(long_int_id),\n            Literal::Marker(marker) => Self::Marker(marker),\n        }\n    }\n}\n\n/// An expression with its source location.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct ExprLoc {\n    pub position: CodeRange,\n    pub expr: Expr,\n}\n\nimpl ExprLoc {\n    pub fn new(position: CodeRange, expr: Expr) -> Self {\n        Self { position, expr }\n    }\n}\n\n/// An AST node parameterized by the function definition type.\n///\n/// This generic enum represents statements in both parsed and prepared forms:\n/// - `Node<RawFunctionDef>` (aka `ParseNode`): Output of the parser, contains unprepared function bodies\n/// - `Node<PreparedFunctionDef>` (aka `PreparedNode`): Output of prepare phase, has resolved names\n///\n/// Some variants (`Pass`, `Global`, `Nonlocal`) only appear in parsed form and are filtered\n/// out during the prepare phase.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum Node<F> {\n    /// No-op statement. Only present in parsed form, filtered out during prepare.\n    Pass,\n    Expr(ExprLoc),\n    Return(ExprLoc),\n    ReturnNone,\n    Raise(Option<ExprLoc>),\n    Assert {\n        test: ExprLoc,\n        msg: Option<ExprLoc>,\n    },\n    Assign {\n        target: Identifier,\n        object: ExprLoc,\n    },\n    /// Tuple unpacking assignment (e.g., `a, b = some_tuple` or `(a, b), c = nested`).\n    ///\n    /// The right-hand side is evaluated, then unpacked into the targets in order.\n    /// Supports nested unpacking like `(a, b), c = ((1, 2), 'x')`.\n    UnpackAssign {\n        /// The targets to unpack into (can be names or nested tuples)\n        targets: Vec<UnpackTarget>,\n        /// Source position covering all targets (for error message caret placement)\n        targets_position: CodeRange,\n        object: ExprLoc,\n    },\n    OpAssign {\n        target: Identifier,\n        op: Operator,\n        object: ExprLoc,\n    },\n    /// Augmented subscript assignment (e.g., `totals[key] += value`).\n    ///\n    /// This evaluates the container and index exactly once, then performs the\n    /// inplace operation on the current item before storing the result back.\n    /// Limiting duplicate evaluation is important because index expressions may\n    /// have side effects and CPython only evaluates them once.\n    SubscriptOpAssign {\n        target: Identifier,\n        index: ExprLoc,\n        op: Operator,\n        object: ExprLoc,\n        /// Position of the subscript expression (e.g., `totals[key]`) for traceback carets.\n        target_position: CodeRange,\n    },\n    SubscriptAssign {\n        target: Identifier,\n        index: ExprLoc,\n        value: ExprLoc,\n        /// Position of the subscript expression (e.g., `lst[10]`) for traceback carets.\n        target_position: CodeRange,\n    },\n    /// Attribute assignment (e.g., `point.x = 5` or `a.b.c = 5`).\n    ///\n    /// Assigns a value to an attribute on an object. For mutable dataclasses,\n    /// this sets the field value. Returns an error for immutable objects.\n    /// Supports chained attribute access on the left-hand side.\n    AttrAssign {\n        object: ExprLoc,\n        attr: EitherStr,\n        target_position: CodeRange,\n        value: ExprLoc,\n    },\n    For {\n        /// Loop target - either a single identifier or tuple unpacking pattern.\n        target: UnpackTarget,\n        iter: ExprLoc,\n        body: Vec<Self>,\n        or_else: Vec<Self>,\n    },\n    /// While loop statement: `while test: body [else: orelse]`\n    ///\n    /// Executes body repeatedly while test is truthy. If the loop exits normally\n    /// (not via break), the else block runs.\n    While {\n        test: ExprLoc,\n        body: Vec<Self>,\n        or_else: Vec<Self>,\n    },\n    /// Break statement - exits the innermost loop.\n    ///\n    /// When executed, control flow jumps past the loop's else block (if any).\n    /// Must be inside a loop, otherwise a `SyntaxError` is raised at compile time.\n    Break {\n        position: CodeRange,\n    },\n    /// Continue statement - jumps to the next iteration of the innermost loop.\n    ///\n    /// When executed, control flow jumps back to the loop's iterator advancement.\n    /// Must be inside a loop, otherwise a `SyntaxError` is raised at compile time.\n    Continue {\n        position: CodeRange,\n    },\n    If {\n        test: ExprLoc,\n        body: Vec<Self>,\n        or_else: Vec<Self>,\n    },\n    FunctionDef(F),\n    /// Global variable declaration. Only present in parsed form, consumed during prepare.\n    ///\n    /// Declares that the listed names refer to module-level (global) variables,\n    /// allowing functions to read and write them instead of creating local variables.\n    Global {\n        position: CodeRange,\n        names: Vec<StringId>,\n    },\n    /// Nonlocal variable declaration. Only present in parsed form, consumed during prepare.\n    ///\n    /// Declares that the listed names refer to variables in enclosing function scopes,\n    /// allowing nested functions to read and write them instead of creating local variables.\n    Nonlocal {\n        position: CodeRange,\n        names: Vec<StringId>,\n    },\n    /// Try/except/else/finally block.\n    ///\n    /// Executes body, catches matching exceptions with handlers, runs else if no exception,\n    /// and always runs finally.\n    Try(Try<Self>),\n    /// Import statement (e.g., `import sys`, `import sys as s`).\n    ///\n    /// Loads a module and binds it to a name in the current namespace.\n    Import {\n        /// The module name to import (e.g., \"sys\", \"typing\").\n        module_name: StringId,\n        /// The binding target - contains the name (or alias), position, and namespace slot.\n        /// After prepare phase, this includes the resolved namespace slot for storing the module.\n        binding: Identifier,\n    },\n    /// From-import statement (e.g., `from typing import TYPE_CHECKING`).\n    ///\n    /// Imports specific names from a module into the current namespace.\n    ImportFrom {\n        /// The module name to import from (e.g., \"typing\").\n        module_name: StringId,\n        /// Names to import: (import_name, binding) pairs.\n        /// The import_name is the name in the module, the binding is the local name\n        /// (alias if provided, otherwise the import name) with resolved namespace slot.\n        names: Vec<(StringId, Identifier)>,\n        /// Source position for error reporting.\n        position: CodeRange,\n    },\n}\n\n/// A prepared function definition with resolved names and scope information.\n///\n/// This is created during the prepare phase and contains everything needed to\n/// compile the function to bytecode. The function body has all names resolved\n/// to namespace indices with proper scoping.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct PreparedFunctionDef {\n    /// The function name identifier with resolved namespace index.\n    pub name: Identifier,\n    /// The function signature with parameter names and default counts.\n    pub signature: Signature,\n    /// The prepared function body with resolved names.\n    pub body: Vec<Node<Self>>,\n    /// Number of local variable slots needed in the namespace.\n    pub namespace_size: usize,\n    /// Enclosing namespace slots for variables captured from enclosing scopes.\n    ///\n    /// At definition time: look up cell HeapId from enclosing namespace at each slot.\n    /// At call time: captured cells are pushed sequentially (our slots are implicit).\n    pub free_var_enclosing_slots: Vec<NamespaceId>,\n    /// Number of cell variables (captured by nested functions).\n    ///\n    /// At call time, this many cells are created and pushed right after params.\n    /// Their slots are implicitly params.len()..params.len()+cell_var_count.\n    pub cell_var_count: usize,\n    /// Maps cell variable indices to their corresponding parameter indices, if any.\n    ///\n    /// When a parameter is also captured by nested functions (cell variable), its value\n    /// must be copied into the cell after binding. Each entry corresponds to a cell\n    /// (index 0..cell_var_count), and contains `Some(param_index)` if that cell is for\n    /// a parameter, or `None` otherwise.\n    pub cell_param_indices: Vec<Option<usize>>,\n    /// Prepared default value expressions, evaluated at function definition time.\n    ///\n    /// Layout: `[pos_defaults...][arg_defaults...][kwarg_defaults...]`\n    /// Each group contains only the parameters that have defaults, in declaration order.\n    /// The counts in `signature` indicate how many defaults exist for each group.\n    pub default_exprs: Vec<ExprLoc>,\n    /// Whether this is an async function (`async def`).\n    ///\n    /// When true, calling this function creates a `Coroutine` object instead of\n    /// immediately pushing a frame.\n    pub is_async: bool,\n}\n\n/// Type alias for prepared AST nodes (output of prepare phase).\npub type PreparedNode = Node<PreparedFunctionDef>;\n\n/// Binary operators for arithmetic, bitwise, and boolean operations.\n///\n/// Uses strum `Display` derive with per-variant serialization for operator symbols.\n#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum Operator {\n    // `+`\n    Add,\n    // `-`\n    Sub,\n    // `*`\n    Mult,\n    // `@`\n    MatMult,\n    // `/`\n    Div,\n    // `%`\n    Mod,\n    // `**`\n    Pow,\n    // `<<`\n    LShift,\n    // `>>`\n    RShift,\n    // `|`\n    BitOr,\n    // `^`\n    BitXor,\n    // `&`\n    BitAnd,\n    // `//`\n    FloorDiv,\n    // bool operators\n    // `and`\n    And,\n    // `or`\n    Or,\n}\n\n/// Defined separately since these operators always return a bool\n#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum CmpOperator {\n    Eq,\n    NotEq,\n    Lt,\n    LtE,\n    Gt,\n    GtE,\n    Is,\n    IsNot,\n    In,\n    NotIn,\n    // we should support floats too, either via a Number type, or ModEqInt and ModEqFloat\n    ModEq(i64),\n}\n"
  },
  {
    "path": "crates/monty/src/fstring.rs",
    "content": "//! F-string type definitions and formatting functions.\n//!\n//! This module contains the AST types for f-strings (formatted string literals)\n//! and the runtime formatting functions used by the bytecode VM.\n//!\n//! F-strings can contain literal text and interpolated expressions with optional\n//! conversion flags (`!s`, `!r`, `!a`) and format specifications.\n\nuse std::str::FromStr;\n\nuse crate::{\n    bytecode::VM,\n    exception_private::{ExcType, RunError, SimpleException},\n    expressions::ExprLoc,\n    intern::StringId,\n    resource::ResourceTracker,\n    types::{PyTrait, Type},\n    value::Value,\n};\n\n// ============================================================================\n// F-string type definitions\n// ============================================================================\n\n/// Conversion flags for f-string interpolations.\n///\n/// These control how the value is converted to string before formatting:\n/// - `None`: Use default string conversion (equivalent to `str()`)\n/// - `Str` (`!s`): Explicitly call `str()`\n/// - `Repr` (`!r`): Call `repr()` for debugging representation\n/// - `Ascii` (`!a`): Call `ascii()` for ASCII-safe representation\n#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum ConversionFlag {\n    #[default]\n    None,\n    /// `!s` - convert using `str()`\n    Str,\n    /// `!r` - convert using `repr()`\n    Repr,\n    /// `!a` - convert using `ascii()` (escapes non-ASCII characters)\n    Ascii,\n}\n\n/// A single part of an f-string.\n///\n/// F-strings are composed of literal text segments and interpolated expressions.\n/// For example, `f\"Hello {name}!\"` has three parts:\n/// - `Literal(interned_hello)` (StringId for \"Hello \")\n/// - `Interpolation { expr: name, ... }`\n/// - `Literal(interned_exclaim)` (StringId for \"!\")\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum FStringPart {\n    /// Literal text segment (e.g., \"Hello \" in `f\"Hello {name}\"`)\n    /// The StringId references the interned string in the Interns table.\n    Literal(StringId),\n    /// Interpolated expression with optional conversion and format spec\n    Interpolation {\n        /// The expression to evaluate\n        expr: Box<ExprLoc>,\n        /// Conversion flag: `None`, `!s` (str), `!r` (repr), `!a` (ascii)\n        conversion: ConversionFlag,\n        /// Optional format specification (can contain nested interpolations)\n        format_spec: Option<FormatSpec>,\n        /// Debug prefix for `=` specifier (e.g., \"a=\" for f'{a=}', \" a = \" for f'{ a = }').\n        /// When present, this text is prepended to the output and repr conversion is used\n        /// by default (unless an explicit conversion is specified).\n        debug_prefix: Option<StringId>,\n    },\n}\n\n/// Format specification for f-string interpolations.\n///\n/// Can be either a pre-parsed static spec or contain nested interpolations.\n/// For example:\n/// - `f\"{value:>10}\"` has `FormatSpec::Static(ParsedFormatSpec { ... })`\n/// - `f\"{value:{width}}\"` has `FormatSpec::Dynamic` with the `width` variable\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum FormatSpec {\n    /// Pre-parsed static format spec (e.g., \">10s\", \".2f\")\n    ///\n    /// Parsing happens at parse time to avoid runtime string parsing overhead.\n    /// Invalid specs cause a parse error immediately.\n    Static(ParsedFormatSpec),\n    /// Dynamic format spec with nested f-string parts\n    ///\n    /// These must be evaluated at runtime, then parsed into a `ParsedFormatSpec`.\n    Dynamic(Vec<FStringPart>),\n}\n\n/// Parsed format specification following Python's format mini-language.\n///\n/// Format: `[[fill]align][sign][z][#][0][width][grouping_option][.precision][type]`\n///\n/// This struct is parsed at parse time for static format specs, avoiding runtime\n/// string parsing. For dynamic format specs, parsing happens after evaluation.\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct ParsedFormatSpec {\n    /// Fill character for padding (default: space)\n    pub fill: char,\n    /// Alignment: '<' (left), '>' (right), '^' (center), '=' (sign-aware)\n    pub align: Option<char>,\n    /// Sign handling: '+' (always), '-' (negative only), ' ' (space for positive)\n    pub sign: Option<char>,\n    /// Whether to zero-pad numbers\n    pub zero_pad: bool,\n    /// Minimum field width\n    pub width: usize,\n    /// Precision for floats or max width for strings\n    pub precision: Option<usize>,\n    /// Type character: 's', 'd', 'f', 'e', 'g', etc.\n    pub type_char: Option<char>,\n}\n\nimpl FromStr for ParsedFormatSpec {\n    type Err = String;\n\n    /// Parses a format specification string into its components.\n    ///\n    /// Returns an error if the specifier contains invalid or unrecognized characters.\n    /// The error includes the original specifier for use in error messages.\n    fn from_str(spec: &str) -> Result<Self, Self::Err> {\n        if spec.is_empty() {\n            return Ok(Self {\n                fill: ' ',\n                ..Default::default()\n            });\n        }\n\n        let mut result = Self {\n            fill: ' ',\n            ..Default::default()\n        };\n        let mut chars = spec.chars().peekable();\n\n        // Parse fill and align: [[fill]align]\n        let first = chars.peek().copied();\n        let second_pos = spec.chars().nth(1);\n\n        if let Some(second) = second_pos {\n            if matches!(second, '<' | '>' | '^' | '=') {\n                // First char is fill, second is align\n                result.fill = first.unwrap_or(' ');\n                chars.next();\n                result.align = chars.next();\n            } else if matches!(first, Some('<' | '>' | '^' | '=')) {\n                result.align = chars.next();\n            }\n        } else if matches!(first, Some('<' | '>' | '^' | '=')) {\n            result.align = chars.next();\n        }\n\n        // Parse sign: +, -, or space\n        if matches!(chars.peek(), Some('+' | '-' | ' ')) {\n            result.sign = chars.next();\n        }\n\n        // Skip '#' (alternate form) for now\n        if chars.peek() == Some(&'#') {\n            chars.next();\n        }\n\n        // Parse zero-padding flag (must come before width)\n        if chars.peek() == Some(&'0') {\n            result.zero_pad = true;\n            chars.next();\n        }\n\n        // Parse width\n        let mut width_str = String::new();\n        while let Some(&c) = chars.peek() {\n            if c.is_ascii_digit() {\n                width_str.push(c);\n                chars.next();\n            } else {\n                break;\n            }\n        }\n        if !width_str.is_empty() {\n            result.width = width_str.parse().unwrap_or(0);\n        }\n\n        // Skip grouping option (comma or underscore)\n        if matches!(chars.peek(), Some(',' | '_')) {\n            chars.next();\n        }\n\n        // Parse precision: .N\n        if chars.peek() == Some(&'.') {\n            chars.next();\n            let mut prec_str = String::new();\n            while let Some(&c) = chars.peek() {\n                if c.is_ascii_digit() {\n                    prec_str.push(c);\n                    chars.next();\n                } else {\n                    break;\n                }\n            }\n            if !prec_str.is_empty() {\n                result.precision = Some(prec_str.parse().unwrap_or(0));\n            }\n        }\n\n        // Parse type character: s, d, f, e, g, etc.\n        if let Some(&c) = chars.peek()\n            && matches!(\n                c,\n                's' | 'd' | 'f' | 'F' | 'e' | 'E' | 'g' | 'G' | 'n' | '%' | 'b' | 'o' | 'x' | 'X' | 'c'\n            )\n        {\n            result.type_char = Some(c);\n            chars.next();\n        }\n\n        // Error if there are any unconsumed characters\n        if chars.peek().is_some() {\n            return Err(spec.to_owned());\n        }\n\n        Ok(result)\n    }\n}\n\n// ============================================================================\n// Format errors\n// ============================================================================\n\n/// Error type for format specification failures.\n///\n/// These errors are returned from formatting functions and should be converted\n/// to appropriate Python exceptions (usually ValueError) by the VM.\n#[derive(Debug, Clone)]\npub enum FormatError {\n    /// Invalid alignment for the given type (e.g., '=' alignment on strings).\n    InvalidAlignment(String),\n    /// Value out of range (e.g., character code > 0x10FFFF).\n    Overflow(String),\n    /// Generic value error (e.g., invalid base, invalid Unicode).\n    ValueError(String),\n}\n\nimpl std::fmt::Display for FormatError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::InvalidAlignment(msg) | Self::Overflow(msg) | Self::ValueError(msg) => {\n                write!(f, \"{msg}\")\n            }\n        }\n    }\n}\n\n/// Formats a value according to a format specification, applying type-appropriate formatting.\n///\n/// Dispatches to the appropriate formatting function based on the value type and format spec:\n/// - Integers: `format_int`, `format_int_base`, `format_char`\n/// - Floats: `format_float_f`, `format_float_e`, `format_float_g`, `format_float_percent`\n/// - Strings: `format_string`\n///\n/// Returns a `ValueError` if the format type character is incompatible with the value type.\npub fn format_with_spec(\n    value: &Value,\n    spec: &ParsedFormatSpec,\n    vm: &VM<'_, '_, impl ResourceTracker>,\n) -> Result<String, RunError> {\n    let value_type = value.py_type(vm.heap);\n\n    match (value, spec.type_char) {\n        // Integer formatting\n        (Value::Int(n), None | Some('d')) => Ok(format_int(*n, spec)),\n        (Value::Int(n), Some('b')) => Ok(format_int_base(*n, 2, spec)?),\n        (Value::Int(n), Some('o')) => Ok(format_int_base(*n, 8, spec)?),\n        (Value::Int(n), Some('x')) => Ok(format_int_base(*n, 16, spec)?),\n        (Value::Int(n), Some('X')) => Ok(format_int_base(*n, 16, spec)?.to_uppercase()),\n        (Value::Int(n), Some('c')) => Ok(format_char(*n, spec)?),\n\n        // Float formatting\n        (Value::Float(f), None | Some('g' | 'G')) => Ok(format_float_g(*f, spec)),\n        (Value::Float(f), Some('f' | 'F')) => Ok(format_float_f(*f, spec)),\n        (Value::Float(f), Some('e')) => Ok(format_float_e(*f, spec, false)),\n        (Value::Float(f), Some('E')) => Ok(format_float_e(*f, spec, true)),\n        (Value::Float(f), Some('%')) => Ok(format_float_percent(*f, spec)),\n\n        // Int to float formatting (Python allows this)\n        (Value::Int(n), Some('f' | 'F')) => Ok(format_float_f(*n as f64, spec)),\n        (Value::Int(n), Some('e')) => Ok(format_float_e(*n as f64, spec, false)),\n        (Value::Int(n), Some('E')) => Ok(format_float_e(*n as f64, spec, true)),\n        (Value::Int(n), Some('g' | 'G')) => Ok(format_float_g(*n as f64, spec)),\n        (Value::Int(n), Some('%')) => Ok(format_float_percent(*n as f64, spec)),\n\n        // String formatting (including InternString and heap strings)\n        (_, None | Some('s')) if value_type == Type::Str => {\n            let s = value.py_str(vm);\n            Ok(format_string(&s, spec)?)\n        }\n\n        // Bool as int\n        (Value::Bool(b), Some('d')) => Ok(format_int(i64::from(*b), spec)),\n\n        // No type specifier: convert to string and format\n        (_, None) => {\n            let s = value.py_str(vm);\n            Ok(format_string(&s, spec)?)\n        }\n\n        // Type mismatch errors\n        (_, Some(c)) => Err(SimpleException::new_msg(\n            ExcType::ValueError,\n            format!(\"Unknown format code '{c}' for object of type '{value_type}'\"),\n        )\n        .into()),\n    }\n}\n\n/// Encodes a ParsedFormatSpec into a u64 for storage in bytecode constants.\n///\n/// Encoding layout (fits in 48 bits):\n/// - bits 0-7: fill character (as ASCII, default space=32)\n/// - bits 8-10: align (0=none, 1='<', 2='>', 3='^', 4='=')\n/// - bits 11-12: sign (0=none, 1='+', 2='-', 3=' ')\n/// - bit 13: zero_pad\n/// - bits 14-29: width (16 bits, max 65535)\n/// - bits 30-45: precision (16 bits, using 0xFFFF as \"no precision\")\n/// - bits 46-50: type_char (0=none, 1-15=explicit type mapping: b,c,d,e,E,f,F,g,G,n,o,s,x,X,%)\npub fn encode_format_spec(spec: &ParsedFormatSpec) -> u64 {\n    let fill = spec.fill as u64;\n    let align = match spec.align {\n        None => 0u64,\n        Some('<') => 1,\n        Some('>') => 2,\n        Some('^') => 3,\n        Some('=') => 4,\n        Some(_) => 0,\n    };\n    let sign = match spec.sign {\n        None => 0u64,\n        Some('+') => 1,\n        Some('-') => 2,\n        Some(' ') => 3,\n        Some(_) => 0,\n    };\n    let zero_pad = u64::from(spec.zero_pad);\n    let width = spec.width as u64;\n    let precision = spec.precision.map_or(0xFFFFu64, |p| p as u64);\n    let type_char = spec.type_char.map_or(0u64, |c| match c {\n        'b' => 1,\n        'c' => 2,\n        'd' => 3,\n        'e' => 4,\n        'E' => 5,\n        'f' => 6,\n        'F' => 7,\n        'g' => 8,\n        'G' => 9,\n        'n' => 10,\n        'o' => 11,\n        's' => 12,\n        'x' => 13,\n        'X' => 14,\n        '%' => 15,\n        _ => 0,\n    });\n\n    fill | (align << 8) | (sign << 11) | (zero_pad << 13) | (width << 14) | (precision << 30) | (type_char << 46)\n}\n\n/// Decodes a u64 back into a ParsedFormatSpec.\n///\n/// Reverses the bit-packing done by `encode_format_spec`. Used by the VM\n/// when executing `FormatValue` to retrieve the format specification from\n/// the constant pool (where it's stored as a negative integer marker).\npub fn decode_format_spec(encoded: u64) -> ParsedFormatSpec {\n    let fill = (encoded & 0xFF) as u8 as char;\n    let align_bits = (encoded >> 8) & 0x07;\n    let sign_bits = (encoded >> 11) & 0x03;\n    let zero_pad = ((encoded >> 13) & 0x01) != 0;\n    let width = ((encoded >> 14) & 0xFFFF) as usize;\n    let precision_raw = ((encoded >> 30) & 0xFFFF) as usize;\n    let type_bits = ((encoded >> 46) & 0x1F) as u8;\n\n    let align = match align_bits {\n        1 => Some('<'),\n        2 => Some('>'),\n        3 => Some('^'),\n        4 => Some('='),\n        _ => None,\n    };\n\n    let sign = match sign_bits {\n        1 => Some('+'),\n        2 => Some('-'),\n        3 => Some(' '),\n        _ => None,\n    };\n\n    let precision = if precision_raw == 0xFFFF {\n        None\n    } else {\n        Some(precision_raw)\n    };\n\n    let type_char = match type_bits {\n        1 => Some('b'),\n        2 => Some('c'),\n        3 => Some('d'),\n        4 => Some('e'),\n        5 => Some('E'),\n        6 => Some('f'),\n        7 => Some('F'),\n        8 => Some('g'),\n        9 => Some('G'),\n        10 => Some('n'),\n        11 => Some('o'),\n        12 => Some('s'),\n        13 => Some('x'),\n        14 => Some('X'),\n        15 => Some('%'),\n        _ => None,\n    };\n\n    ParsedFormatSpec {\n        fill,\n        align,\n        sign,\n        zero_pad,\n        width,\n        precision,\n        type_char,\n    }\n}\n\n// ============================================================================\n// Formatting functions\n// ============================================================================\n\n/// Formats a string value according to a format specification.\n///\n/// Applies the following transformations in order:\n/// 1. Truncation: If `precision` is set, limits the string to that many characters\n/// 2. Alignment: Pads to `width` using `fill` character (default left-aligned for strings)\n///\n/// Returns an error if `=` alignment is used (sign-aware padding only valid for numbers).\npub fn format_string(value: &str, spec: &ParsedFormatSpec) -> Result<String, FormatError> {\n    // Handle precision (string truncation)\n    let value = if let Some(prec) = spec.precision {\n        value.chars().take(prec).collect::<String>()\n    } else {\n        value.to_owned()\n    };\n\n    // Validate alignment for strings (= is only for numbers)\n    if spec.align == Some('=') {\n        return Err(FormatError::InvalidAlignment(\n            \"'=' alignment not allowed in string format specifier\".to_owned(),\n        ));\n    }\n\n    // Default alignment for strings is left ('<')\n    let align = spec.align.unwrap_or('<');\n    Ok(pad_string(&value, spec.width, align, spec.fill))\n}\n\n/// Formats an integer in decimal with a format specification.\n///\n/// Applies the following:\n/// - Sign prefix based on `sign` spec: `+` (always show), `-` (negatives only), ` ` (space for positive)\n/// - Zero-padding: When `zero_pad` is true or `=` alignment, inserts zeros between sign and digits\n/// - Alignment: Right-aligned by default for numbers, pads to `width` with `fill` character\npub fn format_int(n: i64, spec: &ParsedFormatSpec) -> String {\n    let is_negative = n < 0;\n    let abs_str = n.abs().to_string();\n\n    // Build the sign prefix\n    let sign = if is_negative {\n        \"-\"\n    } else {\n        match spec.sign {\n            Some('+') => \"+\",\n            Some(' ') => \" \",\n            _ => \"\",\n        }\n    };\n\n    // Default alignment for numbers is right ('>')\n    let align = spec.align.unwrap_or('>');\n\n    // Handle sign-aware zero-padding or regular padding\n    if spec.zero_pad || align == '=' {\n        let fill = if spec.zero_pad { '0' } else { spec.fill };\n        let total_len = sign.len() + abs_str.len();\n        if spec.width > total_len {\n            let padding = spec.width - total_len;\n            let pad_str: String = std::iter::repeat_n(fill, padding).collect();\n            format!(\"{sign}{pad_str}{abs_str}\")\n        } else {\n            format!(\"{sign}{abs_str}\")\n        }\n    } else {\n        let value = format!(\"{sign}{abs_str}\");\n        pad_string(&value, spec.width, align, spec.fill)\n    }\n}\n\n/// Formats an integer in binary (base 2), octal (base 8), or hexadecimal (base 16).\n///\n/// Used for format types `b`, `o`, `x`, and `X`. The sign is prepended for negative numbers.\n/// Does not include base prefixes like `0b`, `0o`, `0x` (those require the `#` flag which\n/// is not yet implemented). Returns an error for invalid base values.\npub fn format_int_base(n: i64, base: u32, spec: &ParsedFormatSpec) -> Result<String, FormatError> {\n    let is_negative = n < 0;\n    let abs_val = n.unsigned_abs();\n\n    let abs_str = match base {\n        2 => format!(\"{abs_val:b}\"),\n        8 => format!(\"{abs_val:o}\"),\n        16 => format!(\"{abs_val:x}\"),\n        _ => return Err(FormatError::ValueError(\"Invalid base\".to_owned())),\n    };\n\n    let sign = if is_negative { \"-\" } else { \"\" };\n    let value = format!(\"{sign}{abs_str}\");\n\n    let align = spec.align.unwrap_or('>');\n    Ok(pad_string(&value, spec.width, align, spec.fill))\n}\n\n/// Formats an integer as a Unicode character (format type `c`).\n///\n/// Converts the integer to its corresponding Unicode code point. Valid range is 0 to 0x10FFFF.\n/// Returns `Overflow` error if out of range, `ValueError` if not a valid Unicode scalar value\n/// (e.g., surrogate code points). Left-aligned by default like strings.\npub fn format_char(n: i64, spec: &ParsedFormatSpec) -> Result<String, FormatError> {\n    if !(0..=0x0010_FFFF).contains(&n) {\n        return Err(FormatError::Overflow(\"%c arg not in range(0x110000)\".to_owned()));\n    }\n    let n_u32 = u32::try_from(n).expect(\"format_char n validated in 0..=0x10FFFF range\");\n    let c = char::from_u32(n_u32).ok_or_else(|| FormatError::ValueError(\"Invalid Unicode code point\".to_owned()))?;\n    let value = c.to_string();\n    let align = spec.align.unwrap_or('<');\n    Ok(pad_string(&value, spec.width, align, spec.fill))\n}\n\n/// Formats a float in fixed-point notation (format types `f` and `F`).\n///\n/// Always includes a decimal point with `precision` digits after it (default 6).\n/// Handles sign prefix, zero-padding between sign and digits when `zero_pad` or `=` alignment.\n/// Right-aligned by default. NaN and infinity are formatted as `nan`/`inf` (or `NAN`/`INF` for `F`).\npub fn format_float_f(f: f64, spec: &ParsedFormatSpec) -> String {\n    let precision = spec.precision.unwrap_or(6);\n    let is_negative = f.is_sign_negative() && !f.is_nan();\n    let abs_val = f.abs();\n\n    let abs_str = format!(\"{abs_val:.precision$}\");\n\n    let sign = if is_negative {\n        \"-\"\n    } else {\n        match spec.sign {\n            Some('+') => \"+\",\n            Some(' ') => \" \",\n            _ => \"\",\n        }\n    };\n\n    let align = spec.align.unwrap_or('>');\n\n    if spec.zero_pad || align == '=' {\n        let fill = if spec.zero_pad { '0' } else { spec.fill };\n        let total_len = sign.len() + abs_str.len();\n        if spec.width > total_len {\n            let padding = spec.width - total_len;\n            let pad_str: String = std::iter::repeat_n(fill, padding).collect();\n            format!(\"{sign}{pad_str}{abs_str}\")\n        } else {\n            format!(\"{sign}{abs_str}\")\n        }\n    } else {\n        let value = format!(\"{sign}{abs_str}\");\n        pad_string(&value, spec.width, align, spec.fill)\n    }\n}\n\n/// Formats a float in exponential/scientific notation (format types `e` and `E`).\n///\n/// Produces output like `1.234568e+03` with `precision` digits after decimal (default 6).\n/// The `uppercase` parameter controls whether to use `E` or `e` for the exponent marker.\n/// Exponent is always formatted with a sign and at least 2 digits (Python convention).\npub fn format_float_e(f: f64, spec: &ParsedFormatSpec, uppercase: bool) -> String {\n    let precision = spec.precision.unwrap_or(6);\n    let is_negative = f.is_sign_negative() && !f.is_nan();\n    let abs_val = f.abs();\n\n    let abs_str = if uppercase {\n        format!(\"{abs_val:.precision$E}\")\n    } else {\n        format!(\"{abs_val:.precision$e}\")\n    };\n\n    // Fix exponent format to match Python (e+03 not e3)\n    let abs_str = fix_exp_format(&abs_str);\n\n    let sign = if is_negative {\n        \"-\"\n    } else {\n        match spec.sign {\n            Some('+') => \"+\",\n            Some(' ') => \" \",\n            _ => \"\",\n        }\n    };\n\n    let value = format!(\"{sign}{abs_str}\");\n    let align = spec.align.unwrap_or('>');\n    pad_string(&value, spec.width, align, spec.fill)\n}\n\n/// Formats a float in \"general\" format (format types `g` and `G`).\n///\n/// Chooses between fixed-point and exponential notation based on the magnitude:\n/// - Uses exponential if exponent < -4 or >= precision\n/// - Otherwise uses fixed-point notation\n///\n/// Unlike `f` and `e` formats, trailing zeros are stripped from the result.\n/// Default precision is 6, but minimum is 1 significant digit.\npub fn format_float_g(f: f64, spec: &ParsedFormatSpec) -> String {\n    let precision = spec.precision.unwrap_or(6).max(1);\n    let is_negative = f.is_sign_negative() && !f.is_nan();\n    let abs_val = f.abs();\n\n    // Python's g format: use exponential if exponent < -4 or >= precision\n    let exp = if abs_val == 0.0 {\n        0\n    } else {\n        // log10 of valid floats fits in i32; floor() returns a finite f64\n        f64_to_i32_trunc(abs_val.log10().floor())\n    };\n\n    // precision is typically small (default 6), safe to convert to i32\n    let prec_i32 = i32::try_from(precision).unwrap_or(i32::MAX);\n    let abs_str = if exp < -4 || exp >= prec_i32 {\n        // Use exponential notation\n        let exp_prec = precision.saturating_sub(1);\n        let formatted = format!(\"{abs_val:.exp_prec$e}\");\n        // Python strips trailing zeros from the mantissa\n        strip_trailing_zeros_exp(&formatted)\n    } else {\n        // Use fixed notation - result is non-negative due to .max(0)\n        let sig_digits_i32 = (prec_i32 - exp - 1).max(0);\n        let sig_digits = usize::try_from(sig_digits_i32).expect(\"sig_digits guaranteed non-negative\");\n        let formatted = format!(\"{abs_val:.sig_digits$}\");\n        strip_trailing_zeros(&formatted)\n    };\n\n    let sign = if is_negative {\n        \"-\"\n    } else {\n        match spec.sign {\n            Some('+') => \"+\",\n            Some(' ') => \" \",\n            _ => \"\",\n        }\n    };\n\n    let value = format!(\"{sign}{abs_str}\");\n    let align = spec.align.unwrap_or('>');\n    pad_string(&value, spec.width, align, spec.fill)\n}\n\n/// Applies ASCII conversion to a string (escapes non-ASCII characters).\n///\n/// Used for the `!a` conversion flag in f-strings. Takes a string (typically a repr)\n/// and escapes all non-ASCII characters using `\\xNN`, `\\uNNNN`, or `\\UNNNNNNNN`.\npub fn ascii_escape(s: &str) -> String {\n    use std::fmt::Write;\n    let mut result = String::new();\n    for c in s.chars() {\n        if c.is_ascii() {\n            result.push(c);\n        } else {\n            let code = c as u32;\n            if code <= 0xFF {\n                write!(result, \"\\\\x{code:02x}\")\n            } else if code <= 0xFFFF {\n                write!(result, \"\\\\u{code:04x}\")\n            } else {\n                write!(result, \"\\\\U{code:08x}\")\n            }\n            .expect(\"string write should be infallible\");\n        }\n    }\n    result\n}\n\n/// Formats a float as a percentage (format type `%`).\n///\n/// Multiplies the value by 100 and appends a `%` sign. Uses fixed-point notation\n/// with `precision` decimal places (default 6). For example, `0.1234` becomes `12.340000%`.\npub fn format_float_percent(f: f64, spec: &ParsedFormatSpec) -> String {\n    let precision = spec.precision.unwrap_or(6);\n    let percent_val = f * 100.0;\n    let is_negative = percent_val.is_sign_negative() && !percent_val.is_nan();\n    let abs_val = percent_val.abs();\n\n    let abs_str = format!(\"{abs_val:.precision$}%\");\n\n    let sign = if is_negative {\n        \"-\"\n    } else {\n        match spec.sign {\n            Some('+') => \"+\",\n            Some(' ') => \" \",\n            _ => \"\",\n        }\n    };\n\n    let value = format!(\"{sign}{abs_str}\");\n    let align = spec.align.unwrap_or('>');\n    pad_string(&value, spec.width, align, spec.fill)\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\n/// Pads a string to a given width with alignment.\n///\n/// Alignment options:\n/// - '<': left-align (pad on right)\n/// - '>': right-align (pad on left)\n/// - '^': center (pad both sides)\nfn pad_string(value: &str, width: usize, align: char, fill: char) -> String {\n    let value_len = value.chars().count();\n    if width <= value_len {\n        return value.to_owned();\n    }\n\n    let padding = width - value_len;\n\n    match align {\n        '<' => {\n            let mut s = value.to_owned();\n            for _ in 0..padding {\n                s.push(fill);\n            }\n            s\n        }\n        '>' => {\n            let mut s = String::new();\n            for _ in 0..padding {\n                s.push(fill);\n            }\n            s.push_str(value);\n            s\n        }\n        '^' => {\n            let left_pad = padding / 2;\n            let right_pad = padding - left_pad;\n            let mut s = String::new();\n            for _ in 0..left_pad {\n                s.push(fill);\n            }\n            s.push_str(value);\n            for _ in 0..right_pad {\n                s.push(fill);\n            }\n            s\n        }\n        _ => value.to_owned(),\n    }\n}\n\n/// Strips trailing zeros from a decimal float string.\n///\n/// Used by the `:g` format to remove insignificant trailing zeros.\n/// Also removes the decimal point if all fractional digits are stripped.\n/// Has no effect if the string doesn't contain a decimal point.\nfn strip_trailing_zeros(s: &str) -> String {\n    if !s.contains('.') {\n        return s.to_owned();\n    }\n    let trimmed = s.trim_end_matches('0');\n    if let Some(stripped) = trimmed.strip_suffix('.') {\n        stripped.to_owned()\n    } else {\n        trimmed.to_owned()\n    }\n}\n\n/// Strips trailing zeros from a float in exponential notation.\n///\n/// Splits the string at `e` or `E`, strips zeros from the mantissa part,\n/// then recombines with the exponent. Also normalizes the exponent format\n/// to Python's convention (sign and at least 2 digits).\nfn strip_trailing_zeros_exp(s: &str) -> String {\n    if let Some(e_pos) = s.find(['e', 'E']) {\n        let (mantissa, exp_part) = s.split_at(e_pos);\n        let trimmed_mantissa = strip_trailing_zeros(mantissa);\n        let fixed_exp = fix_exp_format(exp_part);\n        format!(\"{trimmed_mantissa}{fixed_exp}\")\n    } else {\n        strip_trailing_zeros(s)\n    }\n}\n\n/// Converts Rust's exponential format to Python's format.\n///\n/// Rust produces \"e3\" or \"e-3\" but Python expects \"e+03\" or \"e-03\".\n/// This function ensures the exponent has:\n/// 1. A sign character ('+' or '-')\n/// 2. At least 2 digits\nfn fix_exp_format(s: &str) -> String {\n    // Find the 'e' or 'E' marker\n    let Some(e_pos) = s.find(['e', 'E']) else {\n        return s.to_owned();\n    };\n\n    let (before_e, e_and_rest) = s.split_at(e_pos);\n    let e_char = e_and_rest.chars().next().unwrap();\n    let exp_part = &e_and_rest[1..];\n\n    // Parse the exponent sign and value\n    let (sign, digits) = if let Some(stripped) = exp_part.strip_prefix('-') {\n        ('-', stripped)\n    } else if let Some(stripped) = exp_part.strip_prefix('+') {\n        ('+', stripped)\n    } else {\n        ('+', exp_part)\n    };\n\n    // Ensure at least 2 digits\n    let padded_digits = if digits.len() < 2 {\n        format!(\"{digits:0>2}\")\n    } else {\n        digits.to_owned()\n    };\n\n    format!(\"{before_e}{e_char}{sign}{padded_digits}\")\n}\n\n/// Truncates f64 to i32 with clamping for out-of-range values.\n///\n/// Used for exponent calculations where the result should fit in i32.\nfn f64_to_i32_trunc(value: f64) -> i32 {\n    if value >= f64::from(i32::MAX) {\n        i32::MAX\n    } else if value <= f64::from(i32::MIN) {\n        i32::MIN\n    } else {\n        // SAFETY for clippy: value is guaranteed to be in (i32::MIN, i32::MAX)\n        // after the bounds checks above, so truncation cannot overflow\n        #[expect(clippy::cast_possible_truncation, reason = \"bounds checked above\")]\n        let result = value as i32;\n        result\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/function.rs",
    "content": "use std::fmt::Write;\n\nuse crate::{bytecode::Code, expressions::Identifier, intern::Interns, namespace::NamespaceId, signature::Signature};\n\n/// A defined function once compiled and ready for execution.\n///\n/// This is created during the compilation phase from a `PreparedFunctionDef`.\n/// Contains everything needed to execute a user-defined function: compiled bytecode,\n/// metadata, and closure information. Functions are stored on the heap and\n/// referenced via HeapId.\n///\n/// # Namespace Layout\n///\n/// The namespace has a predictable layout that allows sequential construction:\n/// ```text\n/// [params...][cell_vars...][free_vars...][locals...]\n/// ```\n/// - Slots 0..signature.param_count(): function parameters (see `Signature` for layout)\n/// - Slots after params: cell refs for variables captured by nested functions\n/// - Slots after cell_vars: free_var refs (captured from enclosing scope)\n/// - Remaining slots: local variables\n///\n/// # Closure Support\n///\n/// - `free_var_enclosing_slots`: Enclosing namespace slots for captured variables.\n///   At definition time, cells are captured from these slots and stored in a Closure.\n///   At call time, they're pushed sequentially after cell_vars.\n/// - `cell_var_count`: Number of cells to create for variables captured by nested functions.\n///   At call time, cells are created and pushed sequentially after params.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) struct Function {\n    /// The function name (used for error messages and repr).\n    pub name: Identifier,\n    /// The function signature.\n    pub signature: Signature,\n    /// Size of the initial namespace (number of local variable slots).\n    pub namespace_size: usize,\n    /// Enclosing namespace slots for variables captured from enclosing scopes.\n    ///\n    /// At definition time: look up cell HeapId from enclosing namespace at each slot.\n    /// At call time: captured cells are pushed sequentially (our slots are implicit).\n    pub free_var_enclosing_slots: Vec<NamespaceId>,\n    /// Number of cell variables (captured by nested functions).\n    ///\n    /// At call time, this many cells are created and pushed right after params.\n    /// Their slots are implicitly params.len()..params.len()+cell_var_count.\n    pub cell_var_count: usize,\n    /// Maps cell variable indices to their corresponding parameter indices, if any.\n    ///\n    /// When a parameter is also captured by nested functions (cell variable), its value\n    /// must be copied into the cell after binding. Each entry corresponds to a cell\n    /// (index 0..cell_var_count), and contains `Some(param_index)` if that cell is for\n    /// a parameter, or `None` otherwise.\n    pub cell_param_indices: Vec<Option<usize>>,\n    /// Number of default parameter values.\n    ///\n    /// At function definition time, this many default values are evaluated and stored\n    /// in a separate defaults array. The signature indicates how these map to parameters.\n    pub defaults_count: usize,\n    /// Whether this is an async function (`async def`).\n    ///\n    /// When true, calling this function creates a `Coroutine` object instead of\n    /// immediately pushing a frame. The coroutine captures the bound arguments\n    /// and starts execution only when awaited.\n    pub is_async: bool,\n    /// Compiled bytecode for this function body.\n    pub code: Code,\n}\n\nimpl Function {\n    /// Create a new compiled function.\n    ///\n    /// This is typically called by the bytecode compiler after compiling a `PreparedFunctionDef`.\n    ///\n    /// # Arguments\n    /// * `name` - The function name identifier\n    /// * `signature` - The function signature with parameter names and defaults\n    /// * `namespace_size` - Number of local variable slots needed\n    /// * `free_var_enclosing_slots` - Enclosing namespace slots for captured variables\n    /// * `cell_var_count` - Number of cells to create for variables captured by nested functions\n    /// * `cell_param_indices` - Maps cell indices to parameter indices for captured parameters\n    /// * `defaults_count` - Number of default parameter values\n    /// * `is_async` - Whether this is an async function\n    /// * `code` - The compiled bytecode for the function body\n    #[expect(clippy::too_many_arguments)]\n    pub fn new(\n        name: Identifier,\n        signature: Signature,\n        namespace_size: usize,\n        free_var_enclosing_slots: Vec<NamespaceId>,\n        cell_var_count: usize,\n        cell_param_indices: Vec<Option<usize>>,\n        defaults_count: usize,\n        is_async: bool,\n        code: Code,\n    ) -> Self {\n        Self {\n            name,\n            signature,\n            namespace_size,\n            free_var_enclosing_slots,\n            cell_var_count,\n            cell_param_indices,\n            defaults_count,\n            is_async,\n            code,\n        }\n    }\n\n    /// Writes the Python repr() string for this function to a formatter.\n    pub fn py_repr_fmt<W: Write>(&self, f: &mut W, interns: &Interns, py_id: usize) -> std::fmt::Result {\n        write!(\n            f,\n            \"<function '{}' at 0x{:x}>\",\n            interns.get_str(self.name.name_id),\n            py_id\n        )\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/heap.rs",
    "content": "use std::{\n    cell::Cell,\n    collections::hash_map::DefaultHasher,\n    hash::{Hash, Hasher},\n    mem::size_of,\n    vec,\n};\n\nuse smallvec::SmallVec;\n\n// Re-export items moved to `heap_traits` so that `crate::heap::HeapGuard` etc. continue\n// to resolve (used by the `defer_drop!` macros and throughout the codebase).\npub(crate) use crate::heap_data::HeapData;\npub(crate) use crate::heap_traits::{ContainsHeap, DropWithHeap, HeapGuard, HeapItem, ImmutableHeapGuard};\nuse crate::{\n    args::ArgValues,\n    asyncio::GatherItem,\n    bytecode::{CallResult, VM},\n    exception_private::{ExcType, RunResult},\n    heap_data::HeapDataMut,\n    intern::Interns,\n    resource::{ResourceError, ResourceTracker, check_mult_size, check_repeat_size},\n    types::{List, LongInt, PyTrait, Tuple, allocate_tuple},\n    value::{EitherStr, Value},\n};\n\n/// Unique identifier for values stored inside the heap arena.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub struct HeapId(usize);\n\nimpl HeapId {\n    /// Returns the raw index value.\n    #[inline]\n    pub fn index(self) -> usize {\n        self.0\n    }\n}\n\n/// The empty tuple is a singleton which is allocated at startup.\nconst EMPTY_TUPLE_ID: HeapId = HeapId(0);\n\n/// Hash caching state stored alongside each heap entry.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\nenum HashState {\n    /// Hash has not yet been computed but the value might be hashable.\n    Unknown,\n    /// Cached hash value for immutable types that have been hashed at least once.\n    Cached(u64),\n    /// Value is unhashable (mutable types or tuples containing unhashables).\n    Unhashable,\n}\n\nimpl HashState {\n    fn for_data(data: &HeapData) -> Self {\n        match data {\n            // Cells are hashable by identity (like all Python objects without __hash__ override)\n            // FrozenSet is immutable and hashable\n            // Range is immutable and hashable\n            // Slice is immutable and hashable (like in CPython)\n            // LongInt is immutable and hashable\n            // NamedTuple is immutable and hashable (like Tuple)\n            HeapData::Str(_)\n            | HeapData::Bytes(_)\n            | HeapData::Tuple(_)\n            | HeapData::NamedTuple(_)\n            | HeapData::FrozenSet(_)\n            | HeapData::Cell(_)\n            | HeapData::Closure(_)\n            | HeapData::FunctionDefaults(_)\n            | HeapData::Range(_)\n            | HeapData::Slice(_)\n            | HeapData::LongInt(_) => Self::Unknown,\n            // Dataclass hashability depends on the mutable flag\n            HeapData::Dataclass(dc) => {\n                if dc.is_frozen() {\n                    Self::Unknown\n                } else {\n                    Self::Unhashable\n                }\n            }\n            // Path is immutable and hashable\n            HeapData::Path(_) => Self::Unknown,\n            // ExtFunction is hashable (by identity, like closures)\n            HeapData::ExtFunction(_) => Self::Unknown,\n            // other types are unhashable\n            _ => Self::Unhashable,\n        }\n    }\n}\n\n/// A single entry inside the heap arena, storing refcount, payload, and hash metadata.\n///\n/// The `hash_state` field tracks whether the heap entry is hashable and, if so,\n/// caches the computed hash. Mutable types (List, Dict) start as `Unhashable` and\n/// will raise TypeError if used as dict keys.\n///\n/// The `data` field is an Option to support temporary borrowing: when methods like\n/// `with_entry_mut` or `call_attr` need mutable access to both the data and the heap,\n/// they can `.take()` the data out (leaving `None`), pass `&mut Heap` to user code,\n/// then restore the data. This avoids unsafe code while keeping `refcount` accessible\n/// for `inc_ref`/`dec_ref` during the borrow.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub struct HeapValue {\n    refcount: Cell<usize>,\n    /// The payload data. Temporarily `None` while borrowed via `with_entry_mut`/`call_attr`.\n    data: Option<HeapData>,\n    /// Current hashing status / cached hash value\n    hash_state: HashState,\n}\n\n/// Zero-size token returned by [`Heap::incr_recursion_depth`].\n///\n/// Represents one level of recursion depth that must be released when the\n/// recursive operation completes. There are two ways to release the token:\n///\n/// - **`DropWithHeap`** — for `&mut Heap` paths (e.g., `py_eq`). Compatible with\n///   `defer_drop!` and `HeapGuard` for automatic cleanup on all code paths.\n/// - **`DropWithImmutableHeap`** — for `&Heap` paths (e.g., `py_repr_fmt`) where\n///   only shared access is available. Compatible with `defer_drop_immutable_heap!`\n///   and `ImmutableHeapGuard`.\n#[derive(Debug)]\npub(crate) struct RecursionToken(());\n\nimpl DropWithHeap for RecursionToken {\n    #[inline]\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        heap.heap().decr_recursion_depth();\n    }\n}\n\n/// Reference-counted arena that backs all heap-only runtime values.\n///\n/// Uses a free list to reuse slots from freed values, keeping memory usage\n/// constant for long-running loops that repeatedly allocate and free values.\n/// When an value is freed via `dec_ref`, its slot ID is added to the free list.\n/// New allocations pop from the free list when available, otherwise append.\n///\n/// Generic over `T: ResourceTracker` to support different resource tracking strategies.\n/// When `T = NoLimitTracker` (the default), all resource checks compile away to no-ops.\n///\n/// Serialization requires `T: Serialize` and `T: Deserialize`. Custom serde implementation\n/// handles the Drop constraint by using `std::mem::take` during serialization.\n#[derive(Debug)]\npub(crate) struct Heap<T: ResourceTracker> {\n    entries: Vec<Option<HeapValue>>,\n    /// IDs of freed slots available for reuse. Populated by `dec_ref`, consumed by `allocate`.\n    free_list: Vec<HeapId>,\n    /// Resource tracker for enforcing limits and scheduling GC.\n    tracker: T,\n    /// True if reference cycles may exist. Set when a container stores a Ref,\n    /// cleared after GC completes. When false, GC can skip mark-sweep entirely.\n    may_have_cycles: bool,\n    /// Number of GC applicable allocations since the last GC.\n    allocations_since_gc: u32,\n    /// Current recursion depth — incremented on function calls and data structure traversals.\n    ///\n    /// Uses `Cell` for interior mutability so that methods with only `&Heap`\n    /// (like `py_repr_fmt`) can still increment/decrement the depth counter.\n    recursion_depth: Cell<usize>,\n}\n\nimpl<T: ResourceTracker + serde::Serialize> serde::Serialize for Heap<T> {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        use serde::ser::SerializeStruct;\n        let mut state = serializer.serialize_struct(\"Heap\", 6)?;\n        state.serialize_field(\"entries\", &self.entries)?;\n        state.serialize_field(\"free_list\", &self.free_list)?;\n        state.serialize_field(\"tracker\", &self.tracker)?;\n        state.serialize_field(\"may_have_cycles\", &self.may_have_cycles)?;\n        state.serialize_field(\"allocations_since_gc\", &self.allocations_since_gc)?;\n        state.end()\n    }\n}\n\nimpl<'de, T: ResourceTracker + serde::Deserialize<'de>> serde::Deserialize<'de> for Heap<T> {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        #[derive(serde::Deserialize)]\n        struct HeapFields<T> {\n            entries: Vec<Option<HeapValue>>,\n            free_list: Vec<HeapId>,\n            tracker: T,\n            may_have_cycles: bool,\n            allocations_since_gc: u32,\n        }\n        let fields = HeapFields::<T>::deserialize(deserializer)?;\n        Ok(Self {\n            entries: fields.entries,\n            free_list: fields.free_list,\n            tracker: fields.tracker,\n            may_have_cycles: fields.may_have_cycles,\n            allocations_since_gc: fields.allocations_since_gc,\n            recursion_depth: Cell::new(0),\n        })\n    }\n}\n\nmacro_rules! take_data {\n    ($self:ident, $id:expr, $func_name:literal) => {\n        $self\n            .entries\n            .get_mut($id.index())\n            .expect(concat!(\"Heap::\", $func_name, \": slot missing\"))\n            .as_mut()\n            .expect(concat!(\"Heap::\", $func_name, \": object already freed\"))\n            .data\n            .take()\n            .expect(concat!(\"Heap::\", $func_name, \": data already borrowed\"))\n    };\n}\n\nmacro_rules! restore_data {\n    ($self:ident, $id:expr, $new_data:expr, $func_name:literal) => {{\n        let entry = $self\n            .entries\n            .get_mut($id.index())\n            .expect(concat!(\"Heap::\", $func_name, \": slot missing\"))\n            .as_mut()\n            .expect(concat!(\"Heap::\", $func_name, \": object already freed\"));\n        entry.data = Some($new_data);\n    }};\n}\n\n/// GC interval - run GC every 100,000 applicable allocations.\n///\n/// This is intentionally infrequent to minimize overhead while still\n/// eventually collecting reference cycles.\nconst GC_INTERVAL: u32 = 100_000;\n\nimpl<T: ResourceTracker> Heap<T> {\n    /// Creates a new heap with the given resource tracker.\n    ///\n    /// Use this to create heaps with custom resource limits or GC scheduling.\n    pub fn new(capacity: usize, tracker: T) -> Self {\n        let mut this = Self {\n            entries: Vec::with_capacity(capacity),\n            free_list: Vec::new(),\n            tracker,\n            may_have_cycles: false,\n            allocations_since_gc: 0,\n            recursion_depth: Cell::new(0),\n        };\n        // TBC: should the empty tuple contribute to the resource limits?\n        // If not, can just place it in `entries` directly without going through `allocate()`.\n        let empty_tuple = this\n            .allocate(HeapData::Tuple(Tuple::default()))\n            .expect(\"Failed to allocate empty tuple singleton\");\n        debug_assert_eq!(empty_tuple, EMPTY_TUPLE_ID);\n        this\n    }\n\n    /// Returns a reference to the resource tracker.\n    pub fn tracker(&self) -> &T {\n        &self.tracker\n    }\n\n    /// Returns a mutable reference to the resource tracker.\n    pub fn tracker_mut(&mut self) -> &mut T {\n        &mut self.tracker\n    }\n\n    /// Checks whether the configured time limit has been exceeded.\n    ///\n    /// Delegates to the resource tracker's `check_time()`. For `NoLimitTracker`,\n    /// this is inlined as a no-op with zero runtime cost. For `LimitTracker`,\n    /// it compares elapsed time against the configured `max_duration_secs`.\n    ///\n    /// Call this inside Rust-side loops (builtins, sort, iterator collection)\n    /// that execute within a single bytecode instruction and would otherwise\n    /// bypass the VM's per-instruction timeout check.\n    #[inline]\n    pub fn check_time(&self) -> Result<(), ResourceError> {\n        self.tracker.check_time()\n    }\n\n    /// Increments the recursion depth and checks the limit via the `ResourceTracker`.\n    ///\n    /// Returns `Ok(RecursionToken)` if within limits. The caller must ensure the\n    /// token is released on all code paths — either via `defer_drop!`/`HeapGuard`\n    /// (for `&mut Heap` contexts) or via `RecursionToken::release()` (for `&Heap` contexts).\n    ///\n    /// Returns `Err(ResourceError::Recursion)` if the limit would be exceeded.\n    #[inline]\n    pub fn incr_recursion_depth(&self) -> Result<RecursionToken, ResourceError> {\n        let depth = self.recursion_depth.get();\n        self.tracker.check_recursion_depth(depth)?;\n        self.recursion_depth.set(depth + 1);\n        Ok(RecursionToken(()))\n    }\n\n    /// Increments the recursion depth, returning `Some(RecursionToken)` if within\n    /// limits, or `None` if the limit is exceeded.\n    ///\n    /// Use this in repr-like contexts where exceeding the limit should produce\n    /// truncated output (e.g., `[...]`) rather than an error.\n    #[inline]\n    pub fn incr_recursion_depth_for_repr(&self) -> Option<RecursionToken> {\n        self.incr_recursion_depth().ok()\n    }\n\n    /// Decrements the recursion depth.\n    ///\n    /// Called internally by `RecursionToken` — prefer releasing the token\n    /// rather than calling this directly.\n    #[inline]\n    pub(crate) fn decr_recursion_depth(&self) {\n        let depth = self.recursion_depth.get();\n        debug_assert!(depth > 0, \"decr_recursion_depth called when depth is 0\");\n        self.recursion_depth.set(depth - 1);\n    }\n\n    /// Returns the current recursion depth.\n    ///\n    /// Used during async task switching to compute a task's depth contribution\n    /// before adjusting the global counter.\n    pub(crate) fn get_recursion_depth(&self) -> usize {\n        self.recursion_depth.get()\n    }\n\n    /// Sets the recursion depth to an explicit value.\n    ///\n    /// Used after deserialization to restore the recursion depth to match\n    /// the number of active (non-global) namespace frames that were serialized.\n    /// Also used during async task switching to subtract/add a task's depth\n    /// contribution when switching away from/to that task.\n    pub(crate) fn set_recursion_depth(&self, depth: usize) {\n        self.recursion_depth.set(depth);\n    }\n\n    /// Number of entries in the heap\n    pub fn size(&self) -> usize {\n        self.entries.len()\n    }\n\n    /// Marks that a reference cycle may exist in the heap.\n    ///\n    /// Call this when a container (list, dict, tuple, etc.) stores a reference\n    /// to another heap object. This enables the GC to skip mark-sweep entirely\n    /// when no cycles are possible.\n    #[inline]\n    pub fn mark_potential_cycle(&mut self) {\n        self.may_have_cycles = true;\n    }\n\n    /// Returns the number of GC-tracked allocations since the last garbage collection.\n    ///\n    /// This counter increments for each allocation of a GC-tracked type (List, Dict, etc.)\n    /// and resets to 0 when `collect_garbage` runs. Useful for testing GC behavior.\n    #[cfg(feature = \"ref-count-return\")]\n    pub fn get_allocations_since_gc(&self) -> u32 {\n        self.allocations_since_gc\n    }\n\n    /// Allocates a new heap entry.\n    ///\n    /// Returns `Err(ResourceError)` if allocation would exceed configured limits.\n    /// Use this when you need to handle resource limit errors gracefully.\n    ///\n    /// Only GC-tracked types (containers that can hold references) count toward the\n    /// GC allocation threshold. Leaf types like strings don't trigger GC.\n    ///\n    /// When allocating a container that contains heap references, marks potential\n    /// cycles to enable garbage collection.\n    pub fn allocate(&mut self, data: HeapData) -> Result<HeapId, ResourceError> {\n        self.tracker.on_allocate(|| data.py_estimate_size())?;\n        if data.is_gc_tracked() {\n            self.allocations_since_gc = self.allocations_since_gc.wrapping_add(1);\n            // Mark potential cycles if this container has heap references.\n            // This is essential for types like Dict where setitem doesn't call\n            // mark_potential_cycle() - the allocation is the only place to detect refs.\n            if data.has_refs() {\n                self.may_have_cycles = true;\n            }\n        }\n\n        let hash_state = HashState::for_data(&data);\n        let new_entry = HeapValue {\n            refcount: Cell::new(1),\n            data: Some(data),\n            hash_state,\n        };\n\n        let id = if let Some(id) = self.free_list.pop() {\n            // Reuse a freed slot\n            self.entries[id.index()] = Some(new_entry);\n            id\n        } else {\n            // No free slots, append new entry\n            let id = self.entries.len();\n            self.entries.push(Some(new_entry));\n            HeapId(id)\n        };\n\n        Ok(id)\n    }\n\n    /// Returns the singleton empty tuple.\n    ///\n    /// In Python, `() is ()` is always `True` because empty tuples are interned.\n    /// This method provides the same optimization by returning the same `HeapId`\n    /// for all empty tuple allocations.\n    ///\n    /// The returned `Value` has its reference count incremented, so the caller\n    /// owns a reference and must call `dec_ref` when done.\n    pub fn get_empty_tuple(&mut self) -> Value {\n        // Return existing singleton with incremented refcount\n        self.inc_ref(EMPTY_TUPLE_ID);\n        Value::Ref(EMPTY_TUPLE_ID)\n    }\n\n    /// Increments the reference count for an existing heap entry.\n    ///\n    /// # Panics\n    /// Panics if the value ID is invalid or the value has already been freed.\n    pub fn inc_ref(&self, id: HeapId) {\n        let value = self\n            .entries\n            .get(id.index())\n            .expect(\"Heap::inc_ref: slot missing\")\n            .as_ref()\n            .expect(\"Heap::inc_ref: object already freed\");\n        value.refcount.update(|r| r + 1);\n    }\n\n    /// Decrements the reference count and frees the value (plus children) once it hits zero.\n    ///\n    /// Uses an iterative work stack instead of recursion to avoid Rust stack overflow\n    /// when freeing deeply nested containers (e.g., a list nested 10,000 levels deep).\n    /// This is analogous to CPython's \"trashcan\" mechanism for safe deallocation.\n    ///\n    /// # Panics\n    /// Panics if the value ID is invalid or the value has already been freed.\n    pub fn dec_ref(&mut self, id: HeapId) {\n        let mut current_id = id;\n        let mut work_stack = Vec::new();\n        loop {\n            let slot = self\n                .entries\n                .get_mut(current_id.index())\n                .expect(\"Heap::dec_ref: slot missing\");\n            let entry = slot.as_mut().expect(\"Heap::dec_ref: object already freed\");\n            if entry.refcount.get() > 1 {\n                entry.refcount.update(|r| r - 1);\n            } else if let Some(value) = slot.take() {\n                // refcount == 1, free the value and add slot to free list for reuse\n                self.free_list.push(current_id);\n\n                // Notify tracker of freed memory\n                if let Some(ref data) = value.data {\n                    self.tracker.on_free(|| data.py_estimate_size());\n                }\n\n                // Collect child IDs and push onto work stack for iterative processing\n                if let Some(mut data) = value.data {\n                    data.py_dec_ref_ids(&mut work_stack);\n                    drop(data);\n                }\n            }\n\n            let Some(next_id) = work_stack.pop() else {\n                break;\n            };\n            current_id = next_id;\n        }\n    }\n\n    /// Returns an immutable reference to the heap data stored at the given ID.\n    ///\n    /// # Panics\n    /// Panics if the value ID is invalid, the value has already been freed,\n    /// or the data is currently borrowed via `with_entry_mut`/`call_attr`.\n    #[must_use]\n    pub fn get(&self, id: HeapId) -> &HeapData {\n        self.entries\n            .get(id.index())\n            .expect(\"Heap::get: slot missing\")\n            .as_ref()\n            .expect(\"Heap::get: object already freed\")\n            .data\n            .as_ref()\n            .expect(\"Heap::get: data currently borrowed\")\n    }\n\n    /// Returns a mutable reference to the heap data stored at the given ID.\n    ///\n    /// # Panics\n    /// Panics if the value ID is invalid, the value has already been freed,\n    /// or the data is currently borrowed via `with_entry_mut`/`call_attr`.\n    pub fn get_mut(&mut self, id: HeapId) -> HeapDataMut<'_> {\n        self.entries\n            .get_mut(id.index())\n            .expect(\"Heap::get_mut: slot missing\")\n            .as_mut()\n            .expect(\"Heap::get_mut: object already freed\")\n            .data\n            .as_mut()\n            .expect(\"Heap::get_mut: data currently borrowed\")\n            .to_mut()\n    }\n\n    /// Returns or computes the hash for the heap entry at the given ID.\n    ///\n    /// Hashes are computed lazily on first use and then cached. Returns\n    /// `Ok(Some(hash))` for immutable types, `Ok(None)` for mutable types,\n    /// or `Err(ResourceError::Recursion)` if the recursion limit is exceeded.\n    ///\n    /// # Panics\n    /// Panics if the value ID is invalid or the value has already been freed.\n    pub fn get_or_compute_hash(&mut self, id: HeapId, interns: &Interns) -> Result<Option<u64>, ResourceError> {\n        let entry = self\n            .entries\n            .get_mut(id.index())\n            .expect(\"Heap::get_or_compute_hash: slot missing\")\n            .as_mut()\n            .expect(\"Heap::get_or_compute_hash: object already freed\");\n\n        match entry.hash_state {\n            HashState::Unhashable => return Ok(None),\n            HashState::Cached(hash) => return Ok(Some(hash)),\n            HashState::Unknown => {}\n        }\n\n        // Handle Cell specially - uses identity-based hashing (like Python cell objects)\n        if let Some(HeapData::Cell(_)) = &entry.data {\n            let mut hasher = DefaultHasher::new();\n            id.hash(&mut hasher);\n            let hash = hasher.finish();\n            entry.hash_state = HashState::Cached(hash);\n            return Ok(Some(hash));\n        }\n\n        // Compute hash lazily - need to temporarily take data to avoid borrow conflict.\n        // IMPORTANT: data must be restored to the entry on ALL paths (including errors)\n        // to avoid dropping HeapData containing Value::Ref without proper cleanup.\n        let mut data = entry.data.take().expect(\"Heap::get_or_compute_hash: data borrowed\");\n        let hash = data.to_mut().compute_hash_if_immutable(self, interns);\n\n        // Restore data before handling the result\n        let entry = self\n            .entries\n            .get_mut(id.index())\n            .expect(\"Heap::get_or_compute_hash: slot missing after compute\")\n            .as_mut()\n            .expect(\"Heap::get_or_compute_hash: object freed during compute\");\n        entry.data = Some(data);\n\n        // Now handle the result and cache if successful\n        let hash = hash?;\n        entry.hash_state = match hash {\n            Some(value) => HashState::Cached(value),\n            None => HashState::Unhashable,\n        };\n        Ok(hash)\n    }\n\n    /// Calls an attribute on the heap entry, returning an `CallResult` that may signal\n    /// OS, external, or method calls.\n    ///\n    /// Temporarily takes ownership of the payload to avoid borrow conflicts when attribute\n    /// implementations also need mutable heap access (e.g. for refcounting).\n    ///\n    /// Returns `CallResult` which may be:\n    /// - `Value(v)` - Method completed synchronously with value `v`\n    /// - `OsCall(func, args)` - Method needs OS operation; VM should yield to host\n    /// - `ExternalCall(id, args)` - Method needs external function call\n    /// - `MethodCall(name, args)` - Dataclass method call; VM should yield to host\n    pub fn call_attr(vm: &mut VM<'_, '_, T>, id: HeapId, attr: &EitherStr, args: ArgValues) -> RunResult<CallResult> {\n        // Take data out so the borrow of self.entries ends\n        let heap = &mut *vm.heap;\n        let mut data = take_data!(heap, id, \"call_attr\");\n\n        let result = data.py_call_attr(id, vm, attr, args);\n\n        // Restore data\n        let heap = &mut *vm.heap;\n        restore_data!(heap, id, data, \"call_attr\");\n        result\n    }\n\n    /// Gives mutable access to a heap entry while allowing reentrant heap usage\n    /// inside the closure (e.g. to read other values or allocate results).\n    ///\n    /// The data is temporarily taken from the heap entry, so the closure can safely\n    /// mutate both the entry data and the heap (e.g. to allocate new values).\n    /// The data is automatically restored after the closure completes.\n    pub fn with_entry_mut<'a, 'p, F, R>(vm: &mut VM<'a, 'p, T>, id: HeapId, f: F) -> R\n    where\n        F: FnOnce(&mut VM<'a, 'p, T>, HeapDataMut) -> R,\n    {\n        // Take data out in a block so the borrow of self.entries ends\n        let heap = &mut *vm.heap;\n        let mut data = take_data!(heap, id, \"with_entry_mut\");\n\n        let result = f(vm, data.to_mut());\n\n        // Restore data\n        let heap = &mut *vm.heap;\n        restore_data!(heap, id, data, \"with_entry_mut\");\n        result\n    }\n\n    /// Temporarily takes ownership of two heap entries so their data can be borrowed\n    /// simultaneously while still permitting mutable access to the VM (e.g. to\n    /// allocate results). Automatically restores both entries after the closure\n    /// finishes executing.\n    ///\n    /// This is a static method that takes `&mut VM` instead of `&mut self` so that\n    /// the closure receives `&mut VM` — matching the `with_entry_mut` pattern and\n    /// allowing the closure to call methods that need `vm` (e.g. `py_eq`).\n    pub fn with_two<'a, 'p, F, R>(vm: &mut VM<'a, 'p, T>, left: HeapId, right: HeapId, f: F) -> R\n    where\n        F: FnOnce(&mut VM<'a, 'p, T>, &HeapData, &HeapData) -> R,\n    {\n        if left == right {\n            // Same value - take data once and pass it twice\n            let heap = &mut *vm.heap;\n            let data = take_data!(heap, left, \"with_two\");\n\n            let result = f(vm, &data, &data);\n\n            let heap = &mut *vm.heap;\n            restore_data!(heap, left, data, \"with_two\");\n            result\n        } else {\n            // Different values - take both\n            let heap = &mut *vm.heap;\n            let left_data = take_data!(heap, left, \"with_two (left)\");\n            let right_data = take_data!(heap, right, \"with_two (right)\");\n\n            let result = f(vm, &left_data, &right_data);\n\n            // Restore in reverse order\n            let heap = &mut *vm.heap;\n            restore_data!(heap, right, right_data, \"with_two (right)\");\n            restore_data!(heap, left, left_data, \"with_two (left)\");\n            result\n        }\n    }\n\n    /// Returns the reference count for the heap entry at the given ID.\n    ///\n    /// This is primarily used for testing reference counting behavior.\n    ///\n    /// # Panics\n    /// Panics if the value ID is invalid or the value has already been freed.\n    #[must_use]\n    #[cfg(feature = \"ref-count-return\")]\n    pub fn get_refcount(&self, id: HeapId) -> usize {\n        self.entries\n            .get(id.index())\n            .expect(\"Heap::get_refcount: slot missing\")\n            .as_ref()\n            .expect(\"Heap::get_refcount: object already freed\")\n            .refcount\n            .get()\n    }\n\n    /// Returns the number of live (non-freed) values on the heap.\n    ///\n    /// This is primarily used for testing to verify that all heap entries\n    /// are accounted for in reference count tests.\n    ///\n    /// Excludes the empty tuple singleton since it's an internal optimization\n    /// detail that persists even when not explicitly used by user code.\n    #[must_use]\n    #[cfg(feature = \"ref-count-return\")]\n    pub fn entry_count(&self) -> usize {\n        // 1.. to skip index 0 which is the empty tuple singleton\n        self.entries[1..].iter().filter(|o| o.is_some()).count()\n    }\n\n    /// Helper for List in-place add: extends the destination vec with items from a heap list.\n    ///\n    /// This method exists to work around borrow checker limitations when List::py_iadd\n    /// needs to read from one heap entry while extending another. By keeping both\n    /// the read and the refcount increments within Heap's impl block, we can use the\n    /// take/restore pattern to avoid the lifetime propagation issues.\n    ///\n    /// Returns `true` if successful, `false` if the source ID is not a List.\n    pub fn iadd_extend_list(&mut self, source_id: HeapId, dest: &mut Vec<Value>) -> bool {\n        if let HeapData::List(list) = self.get(source_id) {\n            let items: Vec<Value> = list.as_slice().iter().map(|v| v.clone_with_heap(self)).collect();\n            dest.extend(items);\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Multiplies a heap-allocated value by an `i64`.\n    ///\n    /// If `id` refers to a `LongInt`, performs integer multiplication with a size\n    /// pre-check. Otherwise, treats `id` as a sequence and `int_val` as the repeat\n    /// count. This avoids multiple `heap.get()` calls by looking up the data once.\n    ///\n    /// Returns `Ok(None)` if the heap entry is neither a LongInt nor a sequence type.\n    pub fn mult_ref_by_i64(&mut self, id: HeapId, int_val: i64) -> RunResult<Option<Value>> {\n        if let HeapData::LongInt(li) = self.get(id) {\n            check_mult_size(li.bits(), i64_bits(int_val), &self.tracker)?;\n            let result = LongInt::new(li.inner().clone()) * LongInt::from(int_val);\n            Ok(Some(result.into_value(self)?))\n        } else {\n            let count = i64_to_repeat_count(int_val)?;\n            self.mult_sequence(id, count)\n        }\n    }\n\n    /// Multiplies two heap-allocated values.\n    ///\n    /// Returns Ok(None) for unsupported type combinations.\n    pub fn mult_heap_values(&mut self, id1: HeapId, id2: HeapId) -> RunResult<Option<Value>> {\n        let (seq_id, count) = match (self.get(id1), self.get(id2)) {\n            (HeapData::LongInt(a), HeapData::LongInt(b)) => {\n                check_mult_size(a.bits(), b.bits(), &self.tracker)?;\n                let result = LongInt::new(a.inner() * b.inner());\n                return Ok(Some(result.into_value(self)?));\n            }\n            (HeapData::LongInt(li), _) => {\n                let count = longint_to_repeat_count(li)?;\n                (id2, count)\n            }\n            (_, HeapData::LongInt(li)) => {\n                let count = longint_to_repeat_count(li)?;\n                (id1, count)\n            }\n            _ => return Ok(None),\n        };\n\n        self.mult_sequence(seq_id, count)\n    }\n\n    /// Multiplies (repeats) a sequence by an integer count.\n    ///\n    /// This method handles sequence repetition for Python's `*` operator when applied\n    /// to sequences (str, bytes, list, tuple). It creates a new heap-allocated sequence\n    /// with the elements repeated `count` times.\n    ///\n    /// # Arguments\n    /// * `id` - HeapId of the sequence to repeat\n    /// * `count` - Number of times to repeat (0 returns empty sequence)\n    ///\n    /// # Returns\n    /// * `Ok(Some(Value))` - The new repeated sequence\n    /// * `Ok(None)` - If the heap entry is not a sequence type\n    /// * `Err` - If allocation fails due to resource limits\n    pub fn mult_sequence(&mut self, id: HeapId, count: usize) -> RunResult<Option<Value>> {\n        match self.get(id) {\n            HeapData::Str(s) => {\n                check_repeat_size(s.len(), count, &self.tracker)?;\n                Ok(Some(Value::Ref(\n                    self.allocate(HeapData::Str(s.as_str().repeat(count).into()))?,\n                )))\n            }\n            HeapData::Bytes(b) => {\n                check_repeat_size(b.len(), count, &self.tracker)?;\n                Ok(Some(Value::Ref(\n                    self.allocate(HeapData::Bytes(b.as_slice().repeat(count).into()))?,\n                )))\n            }\n            HeapData::List(list) => {\n                check_repeat_size(list.len().saturating_mul(size_of::<Value>()), count, &self.tracker)?;\n                let mut result = Vec::with_capacity(list.as_slice().len() * count);\n                for _ in 0..count {\n                    result.extend(list.as_slice().iter().map(|v| v.clone_with_heap(self)));\n                    self.check_time()?;\n                }\n                Ok(Some(Value::Ref(self.allocate(HeapData::List(List::new(result)))?)))\n            }\n            HeapData::Tuple(tuple) => {\n                if count == 0 {\n                    return Ok(Some(self.get_empty_tuple()));\n                }\n                check_repeat_size(\n                    tuple.as_slice().len().saturating_mul(size_of::<Value>()),\n                    count,\n                    &self.tracker,\n                )?;\n                let mut result = SmallVec::with_capacity(tuple.as_slice().len() * count);\n                for _ in 0..count {\n                    result.extend(tuple.as_slice().iter().map(|v| v.clone_with_heap(self)));\n                    self.check_time()?;\n                }\n                Ok(Some(allocate_tuple(result, self)?))\n            }\n            _ => Ok(None),\n        }\n    }\n\n    /// Returns whether garbage collection should run.\n    ///\n    /// True if reference cycles count exist in the heap\n    /// and the number of allocations since the last GC exceeds the interval.\n    #[inline]\n    pub fn should_gc(&self) -> bool {\n        self.may_have_cycles && self.allocations_since_gc >= GC_INTERVAL\n    }\n\n    /// Runs mark-sweep garbage collection to free unreachable cycles.\n    ///\n    /// This method takes a closure that provides an iterator of root HeapIds\n    /// (typically from the VM's globals and stack). It marks all reachable objects starting\n    /// from roots, then sweeps (frees) any unreachable objects.\n    ///\n    /// This is necessary because reference counting alone cannot free cycles\n    /// where objects reference each other but are unreachable from the program.\n    ///\n    /// # Caller Responsibility\n    /// The caller should check `should_gc()` before calling this method.\n    /// If no cycles are possible, the caller can skip GC entirely.\n    ///\n    /// # Arguments\n    /// * `root` - HeapIds that are roots\n    pub fn collect_garbage(&mut self, root: Vec<HeapId>) {\n        // Mark phase: collect all reachable IDs using BFS\n        // Use Vec<bool> instead of HashSet for O(1) operations without hashing overhead\n        let mut reachable: Vec<bool> = vec![false; self.entries.len()];\n        let mut work_list: Vec<HeapId> = root;\n\n        while let Some(id) = work_list.pop() {\n            let idx = id.index();\n            // Skip if out of bounds or already visited\n            if idx >= reachable.len() || reachable[idx] {\n                continue;\n            }\n            reachable[idx] = true;\n\n            // Add children to work list\n            if let Some(Some(entry)) = self.entries.get(idx)\n                && let Some(ref data) = entry.data\n            {\n                collect_child_ids(data, &mut work_list);\n            }\n        }\n\n        // Sweep phase: free unreachable values\n        for (id, value) in self.entries.iter_mut().enumerate() {\n            if reachable[id] {\n                continue;\n            }\n\n            // This entry is unreachable - free it\n            if let Some(value) = value.take() {\n                // Notify tracker of freed memory\n                if let Some(ref data) = value.data {\n                    self.tracker.on_free(|| data.py_estimate_size());\n                }\n\n                self.free_list.push(HeapId(id));\n\n                // Mark Values as Dereferenced when ref-count-panic is enabled\n                #[cfg(feature = \"ref-count-panic\")]\n                if let Some(mut data) = value.data {\n                    data.py_dec_ref_ids(&mut Vec::new());\n                }\n            }\n        }\n\n        // Reset cycle flag after GC - cycles have been collected\n        self.may_have_cycles = false;\n        self.allocations_since_gc = 0;\n    }\n}\n\n/// Computes the number of significant bits in an `i64`.\n///\n/// Returns 0 for zero, otherwise returns the position of the highest set bit\n/// plus one. Uses unsigned absolute value to handle negative numbers correctly.\nfn i64_bits(value: i64) -> u64 {\n    if value == 0 {\n        0\n    } else {\n        u64::from(64 - value.unsigned_abs().leading_zeros())\n    }\n}\n\n/// Converts an `i64` repeat count to `usize` for sequence repetition.\n///\n/// Returns 0 for negative values (Python treats negative repeat counts as 0).\n/// Returns `OverflowError` if the value exceeds `usize::MAX`.\nfn i64_to_repeat_count(n: i64) -> RunResult<usize> {\n    if n <= 0 {\n        Ok(0)\n    } else {\n        usize::try_from(n).map_err(|_| ExcType::overflow_repeat_count().into())\n    }\n}\n\n/// Converts a `LongInt` repeat count to `usize` for sequence repetition.\n///\n/// Returns 0 for negative values (Python treats negative repeat counts as 0).\n/// Returns `OverflowError` if the value exceeds `usize::MAX`.\nfn longint_to_repeat_count(li: &LongInt) -> RunResult<usize> {\n    if li.is_negative() {\n        Ok(0)\n    } else if let Some(count) = li.to_usize() {\n        Ok(count)\n    } else {\n        Err(ExcType::overflow_repeat_count().into())\n    }\n}\n\n/// Collects child HeapIds from a HeapData value for GC traversal.\nfn collect_child_ids(data: &HeapData, work_list: &mut Vec<HeapId>) {\n    match data {\n        HeapData::List(list) => {\n            // Skip iteration if no refs - major GC optimization for lists of primitives\n            if !list.contains_refs() {\n                return;\n            }\n            for value in list.as_slice() {\n                if let Value::Ref(id) = value {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::Tuple(tuple) => {\n            // Skip iteration if no refs - GC optimization for tuples of primitives\n            if !tuple.contains_refs() {\n                return;\n            }\n            for value in tuple.as_slice() {\n                if let Value::Ref(id) = value {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::NamedTuple(nt) => {\n            // Skip iteration if no refs - GC optimization for namedtuples of primitives\n            if !nt.contains_refs() {\n                return;\n            }\n            for value in nt.as_vec() {\n                if let Value::Ref(id) = value {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::Dict(dict) => {\n            // Skip iteration if no refs - major GC optimization for dicts of primitives\n            if !dict.has_refs() {\n                return;\n            }\n            for (k, v) in dict {\n                if let Value::Ref(id) = k {\n                    work_list.push(*id);\n                }\n                if let Value::Ref(id) = v {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::DictKeysView(view) => {\n            work_list.push(view.dict_id());\n        }\n        HeapData::DictItemsView(view) => {\n            work_list.push(view.dict_id());\n        }\n        HeapData::DictValuesView(view) => {\n            work_list.push(view.dict_id());\n        }\n        HeapData::Set(set) => {\n            for value in set.storage().iter() {\n                if let Value::Ref(id) = value {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::FrozenSet(frozenset) => {\n            for value in frozenset.storage().iter() {\n                if let Value::Ref(id) = value {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::Closure(closure) => {\n            // Add captured cells to work list\n            for cell_id in &closure.cells {\n                work_list.push(*cell_id);\n            }\n            // Add default values that are heap references\n            for default in &closure.defaults {\n                if let Value::Ref(id) = default {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::FunctionDefaults(fd) => {\n            // Add default values that are heap references\n            for default in &fd.defaults {\n                if let Value::Ref(id) = default {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::Cell(cell) => {\n            // Cell can contain a reference to another heap value\n            if let Value::Ref(id) = &cell.0 {\n                work_list.push(*id);\n            }\n        }\n        HeapData::Dataclass(dc) => {\n            // Dataclass attrs are stored in a Dict - iterate through entries\n            for (k, v) in dc.attrs() {\n                if let Value::Ref(id) = k {\n                    work_list.push(*id);\n                }\n                if let Value::Ref(id) = v {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::Iter(iter) => {\n            // Iterator holds a reference to the iterable being iterated\n            if let Value::Ref(id) = iter.value() {\n                work_list.push(*id);\n            }\n        }\n        HeapData::Module(m) => {\n            // Module attrs can contain references to heap values\n            if !m.has_refs() {\n                return;\n            }\n            for (k, v) in m.attrs() {\n                if let Value::Ref(id) = k {\n                    work_list.push(*id);\n                }\n                if let Value::Ref(id) = v {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::Coroutine(coro) => {\n            // Add namespace values that are heap references\n            for value in &coro.namespace {\n                if let Value::Ref(id) = value {\n                    work_list.push(*id);\n                }\n            }\n        }\n        HeapData::GatherFuture(gather) => {\n            // Add coroutine HeapIds to work list\n            for item in &gather.items {\n                if let GatherItem::Coroutine(coro_id) = item {\n                    work_list.push(*coro_id);\n                }\n            }\n            // Add result values that are heap references\n            for result in gather.results.iter().flatten() {\n                if let Value::Ref(id) = result {\n                    work_list.push(*id);\n                }\n            }\n        }\n        // Leaf types with no heap references\n        _ => {}\n    }\n}\n\n/// Drop implementation for Heap that marks all contained Objects as Dereferenced\n/// before dropping to prevent panics when the `ref-count-panic` feature is enabled.\n#[cfg(feature = \"ref-count-panic\")]\nimpl<T: ResourceTracker> Drop for Heap<T> {\n    fn drop(&mut self) {\n        // Mark all contained Objects as Dereferenced before dropping.\n        // We use py_dec_ref_ids for this since it handles the marking\n        // (we ignore the collected IDs since we're dropping everything anyway).\n        let mut dummy_stack = Vec::new();\n        for value in self.entries.iter_mut().flatten() {\n            if let Some(data) = &mut value.data {\n                data.py_dec_ref_ids(&mut dummy_stack);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/heap_data.rs",
    "content": "use std::{\n    borrow::Cow,\n    fmt::Write,\n    hash::{DefaultHasher, Hash, Hasher},\n    mem::discriminant,\n};\n\nuse ahash::AHashSet;\nuse num_integer::Integer;\n\nuse crate::{\n    ExcType, ResourceError, ResourceTracker,\n    args::ArgValues,\n    asyncio::{Coroutine, GatherFuture, GatherItem},\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{RunResult, SimpleException},\n    heap::{Heap, HeapId, HeapItem},\n    intern::{FunctionId, Interns},\n    types::{\n        Bytes, Dataclass, Dict, DictItemsView, DictKeysView, DictValuesView, FrozenSet, List, LongInt, Module,\n        MontyIter, NamedTuple, Path, PyTrait, Range, ReMatch, RePattern, Set, Slice, Str, Tuple, Type,\n    },\n    value::{EitherStr, Value},\n};\n\n/// HeapData captures every runtime value that must live in the arena.\n///\n/// Each variant wraps a type that implements `PyTrait`, providing\n/// Python-compatible operations. The trait is manually implemented to dispatch\n/// to the appropriate variant's implementation.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum HeapData {\n    Str(Str),\n    Bytes(Bytes),\n    List(List),\n    Tuple(Tuple),\n    NamedTuple(NamedTuple),\n    Dict(Dict),\n    DictKeysView(DictKeysView),\n    DictItemsView(DictItemsView),\n    DictValuesView(DictValuesView),\n    Set(Set),\n    FrozenSet(FrozenSet),\n    Closure(Closure),\n    FunctionDefaults(FunctionDefaults),\n    /// A cell wrapping a single mutable value for closure support.\n    ///\n    /// Cells enable nonlocal variable access by providing a heap-allocated\n    /// container that can be shared between a function and its nested functions.\n    /// Both the outer function and inner function hold references to the same\n    /// cell, allowing modifications to propagate across scope boundaries.\n    Cell(CellValue),\n    /// A range object (e.g., `range(10)` or `range(1, 10, 2)`).\n    ///\n    /// Stored on the heap to keep `Value` enum small (16 bytes). Range objects\n    /// are immutable and hashable.\n    Range(Range),\n    /// A slice object (e.g., `slice(1, 10, 2)` or from `x[1:10:2]`).\n    ///\n    /// Stored on the heap to keep `Value` enum small. Slice objects represent\n    /// start:stop:step indices for sequence slicing operations.\n    Slice(Slice),\n    /// An exception instance (e.g., `ValueError('message')`).\n    ///\n    /// Stored on the heap to keep `Value` enum small (16 bytes). Exceptions\n    /// are created when exception types are called or when `raise` is executed.\n    Exception(SimpleException),\n    /// A dataclass instance with fields and method references.\n    ///\n    /// Contains a class name, a Dict of field name -> value mappings, and a set\n    /// of method names that trigger external function calls when invoked.\n    Dataclass(Dataclass),\n    /// An iterator for for-loop iteration and the `iter()` type constructor.\n    ///\n    /// Created by the `GetIter` opcode or `iter()` builtin, advanced by `ForIter`.\n    /// Stores iteration state for lists, tuples, strings, ranges, dicts, and sets.\n    Iter(MontyIter),\n    /// An arbitrary precision integer (LongInt).\n    ///\n    /// Stored on the heap to keep `Value` enum at 16 bytes. Python has one `int` type,\n    /// so LongInt is an implementation detail - we use `Value::Int(i64)` for performance\n    /// when values fit, and promote to LongInt on overflow. When LongInt results fit back\n    /// in i64, they are demoted back to `Value::Int` for performance.\n    LongInt(LongInt),\n    /// A Python module (e.g., `sys`, `typing`).\n    ///\n    /// Modules have a name and a dictionary of attributes. They are created by\n    /// import statements and can have refs to other heap values in their attributes.\n    Module(Module),\n    /// A coroutine object from an async function call.\n    ///\n    /// Contains pre-bound arguments and captured cells, ready to be awaited.\n    /// When awaited, a new frame is pushed using the stored namespace.\n    Coroutine(Coroutine),\n    /// A gather() result tracking multiple coroutines/tasks.\n    ///\n    /// Created by asyncio.gather() and spawns tasks when awaited.\n    GatherFuture(GatherFuture),\n    /// A filesystem path from `pathlib.Path`.\n    ///\n    /// Stored on the heap to provide Python-compatible path operations.\n    /// Pure methods (name, parent, etc.) are handled directly by the VM.\n    /// I/O methods (exists, read_text, etc.) yield external function calls.\n    Path(Path),\n    /// A compiled regex pattern from `re.compile()`.\n    ///\n    /// Contains the original pattern string, flags, and compiled regex engine.\n    /// Leaf type: no heap references, not GC-tracked.\n    RePattern(Box<RePattern>),\n    /// A regex match result from a successful regex operation.\n    ///\n    /// Contains the matched text, capture groups, positions, and input string.\n    /// Leaf type: no heap references, not GC-tracked.\n    ReMatch(ReMatch),\n    /// Reference to an external function whose name was not found in the intern table.\n    ///\n    /// Created when the host resolves a `NameLookup` to a callable whose name does not\n    /// match any interned string (e.g., the host returns a function with a different\n    /// `__name__` than the variable it was assigned to). When called, the VM yields\n    /// `FrameExit::ExternalCall` with an `EitherStr::Heap` containing this name.\n    ExtFunction(String),\n}\n\nimpl HeapData {\n    /// Returns whether this heap data type can participate in reference cycles.\n    ///\n    /// Only container types that can hold references to other heap objects need to be\n    /// tracked for GC purposes. Leaf types like Str, Bytes, Range, and Exception cannot\n    /// form cycles and should not count toward the GC allocation threshold.\n    ///\n    /// This optimization allows programs that allocate many leaf objects (like strings)\n    /// to avoid triggering unnecessary GC cycles.\n    #[inline]\n    pub(crate) fn is_gc_tracked(&self) -> bool {\n        matches!(\n            self,\n            Self::List(_)\n                | Self::Tuple(_)\n                | Self::NamedTuple(_)\n                | Self::Dict(_)\n                | Self::DictKeysView(_)\n                | Self::DictItemsView(_)\n                | Self::DictValuesView(_)\n                | Self::Set(_)\n                | Self::FrozenSet(_)\n                | Self::Closure(_)\n                | Self::FunctionDefaults(_)\n                | Self::Cell(_)\n                | Self::Dataclass(_)\n                | Self::Iter(_)\n                | Self::Module(_)\n                | Self::Coroutine(_)\n                | Self::GatherFuture(_)\n        )\n    }\n\n    /// Returns whether this heap data currently contains any heap references (`Value::Ref`).\n    ///\n    /// Used during allocation to determine if this data could create reference cycles.\n    /// When true, `mark_potential_cycle()` should be called to enable GC.\n    ///\n    /// Note: This is separate from `is_gc_tracked()` - a container may be GC-tracked\n    /// (capable of holding refs) but not currently contain any refs.\n    #[inline]\n    pub(crate) fn has_refs(&self) -> bool {\n        match self {\n            Self::List(list) => list.contains_refs(),\n            Self::Tuple(tuple) => tuple.contains_refs(),\n            Self::NamedTuple(nt) => nt.contains_refs(),\n            Self::Dict(dict) => dict.has_refs(),\n            Self::DictKeysView(_) | Self::DictItemsView(_) | Self::DictValuesView(_) => true,\n            Self::Set(set) => set.has_refs(),\n            Self::FrozenSet(fset) => fset.has_refs(),\n            // Closures always have refs when they have captured cells (HeapIds)\n            Self::Closure(closure) => {\n                !closure.cells.is_empty() || closure.defaults.iter().any(|v| matches!(v, Value::Ref(_)))\n            }\n            Self::FunctionDefaults(fd) => fd.defaults.iter().any(|v| matches!(v, Value::Ref(_))),\n            Self::Cell(cell) => matches!(&cell.0, Value::Ref(_)),\n            Self::Dataclass(dc) => dc.has_refs(),\n            Self::Iter(iter) => iter.has_refs(),\n            Self::Module(m) => m.has_refs(),\n            // Coroutines have refs from namespace values (params, cell/free vars)\n            Self::Coroutine(coro) => coro.namespace.iter().any(|v| matches!(v, Value::Ref(_))),\n            // GatherFutures have refs from coroutine items and results\n            Self::GatherFuture(gather) => {\n                gather.items.iter().any(|item| matches!(item, GatherItem::Coroutine(_)))\n                    || gather\n                        .results\n                        .iter()\n                        .any(|r| r.as_ref().is_some_and(|v| matches!(v, Value::Ref(_))))\n            }\n            // Leaf types cannot have refs\n            _ => false,\n        }\n    }\n\n    /// Returns true if this heap data is a coroutine.\n    #[inline]\n    pub fn is_coroutine(&self) -> bool {\n        matches!(self, Self::Coroutine(_))\n    }\n\n    /// Re-cast this as `HeapDataMut` for mutation.\n    ///\n    /// This is an important part of the Heap invariants: we never allow `&mut HeapData`\n    /// outside of the heap module to prevent heap data changing type during execution.\n    pub(crate) fn to_mut(&mut self) -> HeapDataMut<'_> {\n        match self {\n            Self::Str(s) => HeapDataMut::Str(s),\n            Self::Bytes(b) => HeapDataMut::Bytes(b),\n            Self::List(l) => HeapDataMut::List(l),\n            Self::Tuple(t) => HeapDataMut::Tuple(t),\n            Self::NamedTuple(nt) => HeapDataMut::NamedTuple(nt),\n            Self::Dict(d) => HeapDataMut::Dict(d),\n            Self::DictKeysView(view) => HeapDataMut::DictKeysView(view),\n            Self::DictItemsView(view) => HeapDataMut::DictItemsView(view),\n            Self::DictValuesView(view) => HeapDataMut::DictValuesView(view),\n            Self::Set(s) => HeapDataMut::Set(s),\n            Self::FrozenSet(fs) => HeapDataMut::FrozenSet(fs),\n            Self::Closure(closure) => HeapDataMut::Closure(closure),\n            Self::FunctionDefaults(fd) => HeapDataMut::FunctionDefaults(fd),\n            Self::Cell(cell) => HeapDataMut::Cell(cell),\n            Self::Range(r) => HeapDataMut::Range(r),\n            Self::Slice(s) => HeapDataMut::Slice(s),\n            Self::Exception(e) => HeapDataMut::Exception(e),\n            Self::Dataclass(dc) => HeapDataMut::Dataclass(dc),\n            Self::Iter(iter) => HeapDataMut::Iter(iter),\n            Self::LongInt(li) => HeapDataMut::LongInt(li),\n            Self::Module(m) => HeapDataMut::Module(m),\n            Self::Coroutine(coro) => HeapDataMut::Coroutine(coro),\n            Self::GatherFuture(gather) => HeapDataMut::GatherFuture(gather),\n            Self::Path(p) => HeapDataMut::Path(p),\n            Self::ReMatch(m) => HeapDataMut::ReMatch(m),\n            Self::RePattern(p) => HeapDataMut::RePattern(p),\n            Self::ExtFunction(s) => HeapDataMut::ExtFunction(s),\n        }\n    }\n}\n\n/// Mutable reference to `HeapData` inner values\n#[derive(Debug)]\npub(crate) enum HeapDataMut<'a> {\n    Str(&'a mut Str),\n    Bytes(&'a mut Bytes),\n    List(&'a mut List),\n    Tuple(&'a mut Tuple),\n    NamedTuple(&'a mut NamedTuple),\n    Dict(&'a mut Dict),\n    DictKeysView(&'a mut DictKeysView),\n    DictItemsView(&'a mut DictItemsView),\n    DictValuesView(&'a mut DictValuesView),\n    Set(&'a mut Set),\n    FrozenSet(&'a mut FrozenSet),\n    Closure(&'a mut Closure),\n    FunctionDefaults(&'a mut FunctionDefaults),\n    /// A cell wrapping a single mutable value for closure support.\n    ///\n    /// Cells enable nonlocal variable access by providing a heap-allocated\n    /// container that can be shared between a function and its nested functions.\n    /// Both the outer function and inner function hold references to the same\n    /// cell, allowing modifications to propagate across scope boundaries.\n    Cell(&'a mut CellValue),\n    /// A range object (e.g., `range(10)` or `range(1, 10, 2)`).\n    ///\n    /// Stored on the heap to keep `Value` enum small (16 bytes). Range objects\n    /// are immutable and hashable.\n    Range(&'a mut Range),\n    /// A slice object (e.g., `slice(1, 10, 2)` or from `x[1:10:2]`).\n    ///\n    /// Stored on the heap to keep `Value` enum small. Slice objects represent\n    /// start:stop:step indices for sequence slicing operations.\n    Slice(&'a mut Slice),\n    /// An exception instance (e.g., `ValueError('message')`).\n    ///\n    /// Stored on the heap to keep `Value` enum small (16 bytes). Exceptions\n    /// are created when exception types are called or when `raise` is executed.\n    Exception(&'a mut SimpleException),\n    /// A dataclass instance with fields and method references.\n    ///\n    /// Contains a class name, a Dict of field name -> value mappings, and a set\n    /// of method names that trigger external function calls when invoked.\n    Dataclass(&'a mut Dataclass),\n    /// An iterator for for-loop iteration and the `iter()` type constructor.\n    ///\n    /// Created by the `GetIter` opcode or `iter()` builtin, advanced by `ForIter`.\n    /// Stores iteration state for lists, tuples, strings, ranges, dicts, and sets.\n    Iter(&'a mut MontyIter),\n    /// An arbitrary precision integer (LongInt).\n    ///\n    /// Stored on the heap to keep `Value` enum at 16 bytes. Python has one `int` type,\n    /// so LongInt is an implementation detail - we use `Value::Int(i64)` for performance\n    /// when values fit, and promote to LongInt on overflow. When LongInt results fit back\n    /// in i64, they are demoted back to `Value::Int` for performance.\n    LongInt(&'a mut LongInt),\n    /// A Python module (e.g., `sys`, `typing`).\n    ///\n    /// Modules have a name and a dictionary of attributes. They are created by\n    /// import statements and can have refs to other heap values in their attributes.\n    Module(&'a mut Module),\n    /// A coroutine object from an async function call.\n    ///\n    /// Contains pre-bound arguments and captured cells, ready to be awaited.\n    /// When awaited, a new frame is pushed using the stored namespace.\n    Coroutine(&'a mut Coroutine),\n    /// A gather() result tracking multiple coroutines/tasks.\n    ///\n    /// Created by asyncio.gather() and spawns tasks when awaited.\n    GatherFuture(&'a mut GatherFuture),\n    /// A filesystem path from `pathlib.Path`.\n    ///\n    /// Stored on the heap to provide Python-compatible path operations.\n    /// Pure methods (name, parent, etc.) are handled directly by the VM.\n    /// I/O methods (exists, read_text, etc.) yield external function calls.\n    Path(&'a mut Path),\n    /// A regex match result from `re.match()`, `re.search()`, etc.\n    ///\n    /// Stores matched text, capture groups, and positions. All data is owned\n    /// (no heap references), so reference counting is trivial.\n    ReMatch(&'a mut ReMatch),\n    /// A compiled regex pattern from `re.compile()`.\n    ///\n    /// Wraps a compiled regex with the original pattern string and flags.\n    /// Custom serde serializes only the pattern and flags, recompiling on deserialize.\n    RePattern(&'a mut RePattern),\n    /// Reference to an external function where the name was not interned.\n    ///\n    /// Created when the host resolves a name lookup to a callable whose name\n    /// does not match any interned string (e.g., the host returns a function\n    /// with a different `__name__` than the variable it was assigned to).\n    ExtFunction(&'a mut String),\n}\n\n/// Thin wrapper around `Value` which is used in the `Cell` variant above.\n///\n/// The inner value is the cell's mutable payload.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(transparent)]\n#[repr(transparent)]\npub(crate) struct CellValue(pub(crate) Value);\n\nimpl std::ops::Deref for CellValue {\n    type Target = Value;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\n/// A closure: a function that captures variables from enclosing scopes.\n///\n/// Contains a reference to the function definition, a vector of captured cell HeapIds,\n/// and evaluated default values (if any). When the closure is called, these cells are\n/// passed to the RunFrame for variable access. When the closure is dropped, we must\n/// decrement the ref count on each captured cell and each default value.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct Closure {\n    /// The function definition being captured.\n    pub func_id: FunctionId,\n    /// Captured cells from enclosing scopes.\n    pub cells: Vec<HeapId>,\n    /// Evaluated default parameter values (if any).\n    pub defaults: Vec<Value>,\n}\n\n/// A function with evaluated default parameter values (non-closure).\n///\n/// Contains a reference to the function definition and the evaluated default values.\n/// When the function is called, defaults are cloned for missing optional parameters.\n/// When dropped, we must decrement the ref count on each default value.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct FunctionDefaults {\n    /// The function definition being captured.\n    pub func_id: FunctionId,\n    /// Evaluated default parameter values (if any).\n    pub defaults: Vec<Value>,\n}\n\nimpl HeapItem for CellValue {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Value>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.0.py_dec_ref_ids(stack);\n    }\n}\n\nimpl HeapItem for Closure {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n            + self.cells.len() * std::mem::size_of::<HeapId>()\n            + self.defaults.len() * std::mem::size_of::<Value>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Decrement ref count for captured cells\n        stack.extend(self.cells.iter().copied());\n        // Decrement ref count for default values that are heap references\n        for default in &mut self.defaults {\n            default.py_dec_ref_ids(stack);\n        }\n    }\n}\n\nimpl HeapItem for FunctionDefaults {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.defaults.len() * std::mem::size_of::<Value>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Decrement ref count for default values that are heap references\n        for default in &mut self.defaults {\n            default.py_dec_ref_ids(stack);\n        }\n    }\n}\n\nimpl HeapItem for SimpleException {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.arg().map_or(0, String::len)\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // Exceptions don't contain heap references\n    }\n}\n\nimpl HeapItem for LongInt {\n    fn py_estimate_size(&self) -> usize {\n        self.estimate_size()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // LongInt doesn't contain heap references\n    }\n}\n\nimpl HeapItem for Coroutine {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.namespace.len() * std::mem::size_of::<Value>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Decrement ref count for namespace values that are heap references\n        for value in &mut self.namespace {\n            value.py_dec_ref_ids(stack);\n        }\n    }\n}\n\nimpl HeapItem for GatherFuture {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n            + self.items.len() * std::mem::size_of::<GatherItem>()\n            + self.results.len() * std::mem::size_of::<Option<Value>>()\n            + self.pending_calls.len() * std::mem::size_of::<crate::asyncio::CallId>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Decrement ref count for coroutine HeapIds\n        for item in &self.items {\n            if let GatherItem::Coroutine(id) = item {\n                stack.push(*id);\n            }\n        }\n        // Decrement ref count for result values that are heap references\n        for result in self.results.iter_mut().flatten() {\n            result.py_dec_ref_ids(stack);\n        }\n    }\n}\n\nimpl HeapDataMut<'_> {\n    /// Computes hash for immutable heap types that can be used as dict keys.\n    ///\n    /// Returns `Ok(Some(hash))` for immutable types (Str, Bytes, Tuple of hashables).\n    /// Returns `Ok(None)` for mutable types (List, Dict) which cannot be dict keys.\n    /// Returns `Err(ResourceError::Recursion)` if the recursion limit is exceeded\n    /// while hashing deeply nested containers (e.g., tuples of tuples).\n    ///\n    /// This is called lazily when the value is first used as a dict key,\n    /// avoiding unnecessary hash computation for values that are never used as keys.\n    pub fn compute_hash_if_immutable(\n        &self,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> Result<Option<u64>, ResourceError> {\n        match self {\n            // Hash just the actual string or bytes content for consistency with Value::InternString/InternBytes\n            // hence we don't include the discriminant\n            Self::Str(s) => {\n                let mut hasher = DefaultHasher::new();\n                s.as_str().hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            Self::Bytes(b) => {\n                let mut hasher = DefaultHasher::new();\n                b.as_slice().hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            Self::FrozenSet(fs) => {\n                // FrozenSet hash is XOR of element hashes (order-independent)\n                // Recursion depth is checked inside compute_hash\n                fs.compute_hash(heap, interns)\n            }\n            Self::Tuple(t) => {\n                let token = heap.incr_recursion_depth()?;\n                crate::defer_drop!(token, heap);\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                // Tuple is hashable only if all elements are hashable\n                for obj in t.as_slice() {\n                    match obj.py_hash(heap, interns)? {\n                        Some(h) => h.hash(&mut hasher),\n                        None => return Ok(None),\n                    }\n                }\n                Ok(Some(hasher.finish()))\n            }\n            Self::NamedTuple(nt) => {\n                let token = heap.incr_recursion_depth()?;\n                crate::defer_drop!(token, heap);\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                // Hash only by elements (not type_name) to match equality semantics\n                for obj in nt.as_vec() {\n                    match obj.py_hash(heap, interns)? {\n                        Some(h) => h.hash(&mut hasher),\n                        None => return Ok(None),\n                    }\n                }\n                Ok(Some(hasher.finish()))\n            }\n            Self::Closure(closure) => {\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                // TODO, this is NOT proper hashing, we should somehow hash the function properly\n                closure.func_id.hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            Self::FunctionDefaults(fd) => {\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                // TODO, this is NOT proper hashing, we should somehow hash the function properly\n                fd.func_id.hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            Self::Range(range) => {\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                range.start.hash(&mut hasher);\n                range.stop.hash(&mut hasher);\n                range.step.hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            // Dataclass hashability depends on the mutable flag\n            // Recursion depth is checked inside compute_hash\n            Self::Dataclass(dc) => dc.compute_hash(heap, interns),\n            // Slices are immutable and hashable (like in CPython)\n            Self::Slice(slice) => {\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                slice.start.hash(&mut hasher);\n                slice.stop.hash(&mut hasher);\n                slice.step.hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            // Path is immutable and hashable\n            Self::Path(path) => {\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                path.as_str().hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            // LongInt is immutable and hashable\n            Self::LongInt(li) => Ok(Some(li.hash())),\n            // ExtFunction is hashable by name\n            Self::ExtFunction(name) => {\n                let mut hasher = DefaultHasher::new();\n                discriminant(self).hash(&mut hasher);\n                name.hash(&mut hasher);\n                Ok(Some(hasher.finish()))\n            }\n            // other types cannot be hashed (Cell is handled specially in get_or_compute_hash)\n            _ => Ok(None),\n        }\n    }\n}\n\n/// Shared dispatch macro for `PyTrait` methods on `HeapData` and `HeapDataMut`.\n///\n/// Both enums have identical variants (owned vs borrowed) and identical dispatch\n/// logic. This macro eliminates the duplication by generating the match arms for\n/// each method. The caller provides `self` and the method body for each variant.\nmacro_rules! impl_py_trait_dispatch {\n    ($self_ty:ty) => {\n        impl PyTrait for $self_ty {\n            fn py_type(&self, heap: &Heap<impl ResourceTracker>) -> Type {\n                match self {\n                    Self::Str(s) => s.py_type(heap),\n                    Self::Bytes(b) => b.py_type(heap),\n                    Self::List(l) => l.py_type(heap),\n                    Self::Tuple(t) => t.py_type(heap),\n                    Self::NamedTuple(nt) => nt.py_type(heap),\n                    Self::Dict(d) => d.py_type(heap),\n                    Self::DictKeysView(view) => view.py_type(heap),\n                    Self::DictItemsView(view) => view.py_type(heap),\n                    Self::DictValuesView(view) => view.py_type(heap),\n                    Self::Set(s) => s.py_type(heap),\n                    Self::FrozenSet(fs) => fs.py_type(heap),\n                    Self::Closure(_) | Self::FunctionDefaults(_) | Self::ExtFunction(_) => Type::Function,\n                    Self::Cell(_) => Type::Cell,\n                    Self::Range(_) => Type::Range,\n                    Self::Slice(_) => Type::Slice,\n                    Self::Exception(e) => e.py_type(),\n                    Self::Dataclass(dc) => dc.py_type(heap),\n                    Self::Iter(_) => Type::Iterator,\n                    // LongInt is still `int` in Python - it's an implementation detail\n                    Self::LongInt(_) => Type::Int,\n                    Self::Module(_) => Type::Module,\n                    Self::Coroutine(_) | Self::GatherFuture(_) => Type::Coroutine,\n                    Self::Path(p) => p.py_type(heap),\n                    Self::ReMatch(m) => m.py_type(heap),\n                    Self::RePattern(p) => p.py_type(heap),\n                }\n            }\n\n            fn py_len(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n                match self {\n                    Self::Str(s) => s.py_len(vm),\n                    Self::Bytes(b) => b.py_len(vm),\n                    Self::List(l) => l.py_len(vm),\n                    Self::Tuple(t) => t.py_len(vm),\n                    Self::NamedTuple(nt) => nt.py_len(vm),\n                    Self::Dict(d) => d.py_len(vm),\n                    Self::DictKeysView(view) => view.py_len(vm),\n                    Self::DictItemsView(view) => view.py_len(vm),\n                    Self::DictValuesView(view) => view.py_len(vm),\n                    Self::Set(s) => s.py_len(vm),\n                    Self::FrozenSet(fs) => fs.py_len(vm),\n                    Self::Range(r) => Some(r.len()),\n                    // other types don't have length\n                    _ => None,\n                }\n            }\n\n            fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n                match (self, other) {\n                    (Self::Str(a), Self::Str(b)) => a.py_eq(b, vm),\n                    (Self::Bytes(a), Self::Bytes(b)) => a.py_eq(b, vm),\n                    (Self::List(a), Self::List(b)) => a.py_eq(b, vm),\n                    (Self::Tuple(a), Self::Tuple(b)) => a.py_eq(b, vm),\n                    (Self::NamedTuple(a), Self::NamedTuple(b)) => a.py_eq(b, vm),\n                    // NamedTuple can compare with Tuple by elements (matching CPython behavior)\n                    (Self::NamedTuple(nt), Self::Tuple(t)) | (Self::Tuple(t), Self::NamedTuple(nt)) => {\n                        let nt_items = nt.as_vec();\n                        let t_items = t.as_slice();\n                        if nt_items.len() != t_items.len() {\n                            return Ok(false);\n                        }\n                        // Helper function pattern: acquire token, call helper, drop token.\n                        // Cannot use defer_drop! here because the helper needs &mut VM.\n                        let token = vm.heap.incr_recursion_depth()?;\n                        defer_drop!(token, vm);\n                        for (a, b) in nt_items.iter().zip(t_items.iter()) {\n                            if !a.py_eq(b, vm)? {\n                                return Ok(false);\n                            }\n                        }\n                        Ok(true)\n                    }\n                    (Self::Dict(a), Self::Dict(b)) => a.py_eq(b, vm),\n                    (Self::DictKeysView(a), Self::DictKeysView(b)) => a.py_eq(b, vm),\n                    (Self::DictItemsView(a), Self::DictItemsView(b)) => a.py_eq(b, vm),\n                    (Self::DictValuesView(_), Self::DictValuesView(_)) => Ok(false),\n                    (Self::DictKeysView(a), Self::Set(b)) | (Self::Set(b), Self::DictKeysView(a)) => a.eq_set(b, vm),\n                    (Self::DictKeysView(a), Self::FrozenSet(b)) | (Self::FrozenSet(b), Self::DictKeysView(a)) => {\n                        a.eq_frozenset(b, vm)\n                    }\n                    (Self::DictItemsView(a), Self::Set(b)) | (Self::Set(b), Self::DictItemsView(a)) => a.eq_set(b, vm),\n                    (Self::DictItemsView(a), Self::FrozenSet(b)) | (Self::FrozenSet(b), Self::DictItemsView(a)) => {\n                        a.eq_frozenset(b, vm)\n                    }\n                    (Self::Set(a), Self::Set(b)) => a.py_eq(b, vm),\n                    (Self::FrozenSet(a), Self::FrozenSet(b)) => a.py_eq(b, vm),\n                    (Self::Closure(a), Self::Closure(b)) => Ok(a.func_id == b.func_id && a.cells == b.cells),\n                    (Self::FunctionDefaults(a), Self::FunctionDefaults(b)) => Ok(a.func_id == b.func_id),\n                    (Self::Range(a), Self::Range(b)) => a.py_eq(b, vm),\n                    (Self::Dataclass(a), Self::Dataclass(b)) => a.py_eq(b, vm),\n                    // LongInt equality\n                    (Self::LongInt(a), Self::LongInt(b)) => Ok(a == b),\n                    // Slice equality\n                    (Self::Slice(a), Self::Slice(b)) => a.py_eq(b, vm),\n                    // Path equality\n                    (Self::Path(a), Self::Path(b)) => a.py_eq(b, vm),\n                    // ReMatch objects are not comparable\n                    (Self::ReMatch(a), Self::ReMatch(b)) => a.py_eq(b, vm),\n                    // RePattern equality by pattern string and flags\n                    (Self::RePattern(a), Self::RePattern(b)) => a.py_eq(b, vm),\n                    // Cells, Exceptions, Iterators, Modules, and async types compare by identity only\n                    // (handled at Value level via HeapId comparison)\n                    (Self::Cell(_), Self::Cell(_))\n                    | (Self::Exception(_), Self::Exception(_))\n                    | (Self::Iter(_), Self::Iter(_))\n                    | (Self::Module(_), Self::Module(_))\n                    | (Self::Coroutine(_), Self::Coroutine(_))\n                    | (Self::GatherFuture(_), Self::GatherFuture(_)) => Ok(false),\n                    _ => Ok(false), // Different types are never equal\n                }\n            }\n\n            fn py_cmp(\n                &self,\n                other: &Self,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n            ) -> Result<Option<std::cmp::Ordering>, ResourceError> {\n                match (self, other) {\n                    (Self::Str(a), Self::Str(b)) => a.py_cmp(b, vm),\n                    (Self::Bytes(a), Self::Bytes(b)) => a.py_cmp(b, vm),\n                    (Self::Tuple(a), Self::Tuple(b)) => a.py_cmp(b, vm),\n                    _ => Ok(None),\n                }\n            }\n\n            fn py_bool(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n                match self {\n                    Self::Str(s) => s.py_bool(vm),\n                    Self::Bytes(b) => b.py_bool(vm),\n                    Self::List(l) => l.py_bool(vm),\n                    Self::Tuple(t) => t.py_bool(vm),\n                    Self::NamedTuple(nt) => nt.py_bool(vm),\n                    Self::Dict(d) => d.py_bool(vm),\n                    Self::DictKeysView(view) => view.py_bool(vm),\n                    Self::DictItemsView(view) => view.py_bool(vm),\n                    Self::DictValuesView(view) => view.py_bool(vm),\n                    Self::Set(s) => s.py_bool(vm),\n                    Self::FrozenSet(fs) => fs.py_bool(vm),\n                    Self::Closure(_) | Self::FunctionDefaults(_) | Self::ExtFunction(_) => true,\n                    Self::Cell(_) => true, // Cells are always truthy\n                    Self::Range(r) => r.py_bool(vm),\n                    Self::Slice(s) => s.py_bool(vm),\n                    Self::Exception(_) => true, // Exceptions are always truthy\n                    Self::Dataclass(dc) => dc.py_bool(vm),\n                    Self::Iter(_) => true, // Iterators are always truthy\n                    Self::LongInt(li) => !li.is_zero(),\n                    Self::Module(_) => true,       // Modules are always truthy\n                    Self::Coroutine(_) => true,    // Coroutines are always truthy\n                    Self::GatherFuture(_) => true, // GatherFutures are always truthy\n                    Self::Path(p) => p.py_bool(vm),\n                    Self::ReMatch(m) => m.py_bool(vm),\n                    Self::RePattern(p) => p.py_bool(vm),\n                }\n            }\n\n            fn py_repr_fmt(\n                &self,\n                f: &mut impl Write,\n                vm: &VM<'_, '_, impl ResourceTracker>,\n                heap_ids: &mut AHashSet<HeapId>,\n            ) -> std::fmt::Result {\n                match self {\n                    Self::Str(s) => s.py_repr_fmt(f, vm, heap_ids),\n                    Self::Bytes(b) => b.py_repr_fmt(f, vm, heap_ids),\n                    Self::List(l) => l.py_repr_fmt(f, vm, heap_ids),\n                    Self::Tuple(t) => t.py_repr_fmt(f, vm, heap_ids),\n                    Self::NamedTuple(nt) => nt.py_repr_fmt(f, vm, heap_ids),\n                    Self::Dict(d) => d.py_repr_fmt(f, vm, heap_ids),\n                    Self::DictKeysView(view) => view.py_repr_fmt(f, vm, heap_ids),\n                    Self::DictItemsView(view) => view.py_repr_fmt(f, vm, heap_ids),\n                    Self::DictValuesView(view) => view.py_repr_fmt(f, vm, heap_ids),\n                    Self::Set(s) => s.py_repr_fmt(f, vm, heap_ids),\n                    Self::FrozenSet(fs) => fs.py_repr_fmt(f, vm, heap_ids),\n                    Self::Closure(closure) => vm\n                        .interns\n                        .get_function(closure.func_id)\n                        .py_repr_fmt(f, vm.interns, 0),\n                    Self::FunctionDefaults(fd) => vm.interns.get_function(fd.func_id).py_repr_fmt(f, vm.interns, 0),\n                    // Cell repr shows the contained value's type\n                    Self::Cell(cell) => write!(f, \"<cell: {} object>\", cell.0.py_type(vm.heap)),\n                    Self::Range(r) => r.py_repr_fmt(f, vm, heap_ids),\n                    Self::Slice(s) => s.py_repr_fmt(f, vm, heap_ids),\n                    Self::Exception(e) => e.py_repr_fmt(f),\n                    Self::Dataclass(dc) => dc.py_repr_fmt(f, vm, heap_ids),\n                    Self::Iter(_) => write!(f, \"<iterator>\"),\n                    Self::LongInt(li) => write!(f, \"{li}\"),\n                    Self::Module(m) => write!(f, \"<module '{}'>\", vm.interns.get_str(m.name())),\n                    Self::Coroutine(coro) => {\n                        let func = vm.interns.get_function(coro.func_id);\n                        let name = vm.interns.get_str(func.name.name_id);\n                        write!(f, \"<coroutine object {name}>\")\n                    }\n                    Self::GatherFuture(gather) => write!(f, \"<gather({})>\", gather.item_count()),\n                    Self::Path(p) => p.py_repr_fmt(f, vm, heap_ids),\n                    Self::ReMatch(m) => m.py_repr_fmt(f, vm, heap_ids),\n                    Self::RePattern(p) => p.py_repr_fmt(f, vm, heap_ids),\n                    Self::ExtFunction(name) => write!(f, \"<function '{name}' external>\"),\n                }\n            }\n\n            fn py_str(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Cow<'static, str> {\n                match self {\n                    // Strings return their value directly without quotes\n                    Self::Str(s) => s.py_str(vm),\n                    // LongInt returns its string representation\n                    Self::LongInt(li) => Cow::Owned(li.to_string()),\n                    // Exceptions return just the message (or empty string if no message)\n                    Self::Exception(e) => Cow::Owned(e.py_str()),\n                    // Paths return the path string without the PosixPath() wrapper\n                    Self::Path(p) => Cow::Owned(p.as_str().to_owned()),\n                    // All other types use repr\n                    _ => self.py_repr(vm),\n                }\n            }\n\n            fn py_add(\n                &self,\n                other: &Self,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n            ) -> Result<Option<Value>, ResourceError> {\n                match (self, other) {\n                    (Self::Str(a), Self::Str(b)) => a.py_add(b, vm),\n                    (Self::Bytes(a), Self::Bytes(b)) => a.py_add(b, vm),\n                    (Self::List(a), Self::List(b)) => a.py_add(b, vm),\n                    (Self::Tuple(a), Self::Tuple(b)) => a.py_add(b, vm),\n                    (Self::Dict(a), Self::Dict(b)) => a.py_add(b, vm),\n                    (Self::LongInt(a), Self::LongInt(b)) => {\n                        let bi = a.inner() + b.inner();\n                        Ok(LongInt::new(bi).into_value(vm.heap).map(Some)?)\n                    }\n                    // Cells and Dataclasses don't support arithmetic operations\n                    _ => Ok(None),\n                }\n            }\n\n            fn py_sub(\n                &self,\n                other: &Self,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n            ) -> Result<Option<Value>, ResourceError> {\n                match (self, other) {\n                    (Self::Str(a), Self::Str(b)) => a.py_sub(b, vm),\n                    (Self::Bytes(a), Self::Bytes(b)) => a.py_sub(b, vm),\n                    (Self::List(a), Self::List(b)) => a.py_sub(b, vm),\n                    (Self::Tuple(a), Self::Tuple(b)) => a.py_sub(b, vm),\n                    (Self::Dict(a), Self::Dict(b)) => a.py_sub(b, vm),\n                    (Self::Set(a), Self::Set(b)) => a.py_sub(b, vm),\n                    (Self::FrozenSet(a), Self::FrozenSet(b)) => a.py_sub(b, vm),\n                    (Self::LongInt(a), Self::LongInt(b)) => {\n                        let bi = a.inner() - b.inner();\n                        Ok(LongInt::new(bi).into_value(vm.heap).map(Some)?)\n                    }\n                    // Cells don't support arithmetic operations\n                    _ => Ok(None),\n                }\n            }\n\n            fn py_mod(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n                match (self, other) {\n                    (Self::Str(a), Self::Str(b)) => a.py_mod(b, vm),\n                    (Self::Bytes(a), Self::Bytes(b)) => a.py_mod(b, vm),\n                    (Self::List(a), Self::List(b)) => a.py_mod(b, vm),\n                    (Self::Tuple(a), Self::Tuple(b)) => a.py_mod(b, vm),\n                    (Self::Dict(a), Self::Dict(b)) => a.py_mod(b, vm),\n                    (Self::LongInt(a), Self::LongInt(b)) => {\n                        if b.is_zero() {\n                            Err(ExcType::zero_division().into())\n                        } else {\n                            let bi = a.inner().mod_floor(b.inner());\n                            Ok(LongInt::new(bi).into_value(vm.heap).map(Some)?)\n                        }\n                    }\n                    // Cells don't support arithmetic operations\n                    _ => Ok(None),\n                }\n            }\n\n            fn py_mod_eq(&self, other: &Self, right_value: i64) -> Option<bool> {\n                match (self, other) {\n                    (Self::Str(a), Self::Str(b)) => a.py_mod_eq(b, right_value),\n                    (Self::Bytes(a), Self::Bytes(b)) => a.py_mod_eq(b, right_value),\n                    (Self::List(a), Self::List(b)) => a.py_mod_eq(b, right_value),\n                    (Self::Tuple(a), Self::Tuple(b)) => a.py_mod_eq(b, right_value),\n                    (Self::Dict(a), Self::Dict(b)) => a.py_mod_eq(b, right_value),\n                    // Cells don't support arithmetic operations\n                    _ => None,\n                }\n            }\n\n            fn py_iadd(\n                &mut self,\n                other: &Value,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n                self_id: Option<HeapId>,\n            ) -> Result<bool, ResourceError> {\n                match self {\n                    Self::List(list) => list.py_iadd(other, vm, self_id),\n                    Self::Dict(dict) => dict.py_iadd(other, vm, self_id),\n                    _ => Ok(false),\n                }\n            }\n\n            fn py_call_attr(\n                &mut self,\n                self_id: HeapId,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n                attr: &EitherStr,\n                args: ArgValues,\n            ) -> RunResult<CallResult> {\n                match self {\n                    Self::Str(s) => s.py_call_attr(self_id, vm, attr, args),\n                    Self::Bytes(b) => b.py_call_attr(self_id, vm, attr, args),\n                    Self::List(l) => l.py_call_attr(self_id, vm, attr, args),\n                    Self::Tuple(t) => t.py_call_attr(self_id, vm, attr, args),\n                    Self::Dict(d) => d.py_call_attr(self_id, vm, attr, args),\n                    Self::DictKeysView(view) => view.py_call_attr(self_id, vm, attr, args),\n                    Self::DictItemsView(view) => view.py_call_attr(self_id, vm, attr, args),\n                    Self::DictValuesView(view) => view.py_call_attr(self_id, vm, attr, args),\n                    Self::Set(s) => s.py_call_attr(self_id, vm, attr, args),\n                    Self::FrozenSet(fs) => fs.py_call_attr(self_id, vm, attr, args),\n                    Self::Dataclass(dc) => dc.py_call_attr(self_id, vm, attr, args),\n                    Self::Path(p) => p.py_call_attr(self_id, vm, attr, args),\n                    Self::Module(m) => m.py_call_attr(self_id, vm, attr, args),\n                    Self::ReMatch(m) => m.py_call_attr(self_id, vm, attr, args),\n                    Self::RePattern(p) => p.py_call_attr(self_id, vm, attr, args),\n                    _ => Err(ExcType::attribute_error(\n                        self.py_type(vm.heap),\n                        attr.as_str(vm.interns),\n                    )),\n                }\n            }\n\n            fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n                match self {\n                    Self::Str(s) => s.py_getitem(key, vm),\n                    Self::Bytes(b) => b.py_getitem(key, vm),\n                    Self::List(l) => l.py_getitem(key, vm),\n                    Self::Tuple(t) => t.py_getitem(key, vm),\n                    Self::NamedTuple(nt) => nt.py_getitem(key, vm),\n                    Self::Dict(d) => d.py_getitem(key, vm),\n                    Self::Range(r) => r.py_getitem(key, vm),\n                    Self::ReMatch(m) => m.py_getitem(key, vm),\n                    _ => Err(ExcType::type_error_not_sub(self.py_type(vm.heap))),\n                }\n            }\n\n            fn py_setitem(\n                &mut self,\n                key: Value,\n                value: Value,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n            ) -> RunResult<()> {\n                match self {\n                    Self::Str(s) => s.py_setitem(key, value, vm),\n                    Self::Bytes(b) => b.py_setitem(key, value, vm),\n                    Self::List(l) => l.py_setitem(key, value, vm),\n                    Self::Tuple(t) => t.py_setitem(key, value, vm),\n                    Self::Dict(d) => d.py_setitem(key, value, vm),\n                    _ => Err(ExcType::type_error_not_sub_assignment(self.py_type(vm.heap))),\n                }\n            }\n\n            fn py_getattr(\n                &self,\n                attr: &EitherStr,\n                vm: &mut VM<'_, '_, impl ResourceTracker>,\n            ) -> RunResult<Option<CallResult>> {\n                match self {\n                    Self::Dataclass(dc) => dc.py_getattr(attr, vm),\n                    Self::Module(m) => Ok(m.py_getattr(attr, vm.heap, vm.interns)),\n                    Self::NamedTuple(nt) => nt.py_getattr(attr, vm),\n                    Self::Slice(s) => s.py_getattr(attr, vm),\n                    Self::Exception(exc) => exc.py_getattr(attr, vm.heap, vm.interns),\n                    Self::Path(p) => p.py_getattr(attr, vm),\n                    Self::ReMatch(m) => m.py_getattr(attr, vm),\n                    Self::RePattern(p) => p.py_getattr(attr, vm),\n                    // All other types don't support attribute access via py_getattr\n                    _ => Ok(None),\n                }\n            }\n        }\n    };\n}\n\nimpl_py_trait_dispatch!(HeapDataMut<'_>);\nimpl_py_trait_dispatch!(HeapData);\n\n/// Shared dispatch macro for `HeapItem` methods on `HeapData` and `HeapDataMut`.\n///\n/// Dispatches `py_estimate_size` and `py_dec_ref_ids` to the inner type's\n/// `HeapItem` implementation. For types without a dedicated `HeapItem` impl\n/// (like `ExtFunction` wrapping `String`), the logic is inlined here.\nmacro_rules! impl_heap_item_dispatch {\n    ($self_ty:ty) => {\n        impl HeapItem for $self_ty {\n            fn py_estimate_size(&self) -> usize {\n                match self {\n                    Self::Str(s) => s.py_estimate_size(),\n                    Self::Bytes(b) => b.py_estimate_size(),\n                    Self::List(l) => l.py_estimate_size(),\n                    Self::Tuple(t) => t.py_estimate_size(),\n                    Self::NamedTuple(nt) => nt.py_estimate_size(),\n                    Self::Dict(d) => d.py_estimate_size(),\n                    Self::DictKeysView(view) => view.py_estimate_size(),\n                    Self::DictItemsView(view) => view.py_estimate_size(),\n                    Self::DictValuesView(view) => view.py_estimate_size(),\n                    Self::Set(s) => s.py_estimate_size(),\n                    Self::FrozenSet(fs) => fs.py_estimate_size(),\n                    Self::Closure(closure) => closure.py_estimate_size(),\n                    Self::FunctionDefaults(fd) => fd.py_estimate_size(),\n                    Self::Cell(cell) => cell.py_estimate_size(),\n                    Self::Range(r) => r.py_estimate_size(),\n                    Self::Slice(s) => s.py_estimate_size(),\n                    Self::Exception(e) => e.py_estimate_size(),\n                    Self::Dataclass(dc) => dc.py_estimate_size(),\n                    Self::Iter(iter) => iter.py_estimate_size(),\n                    Self::LongInt(li) => li.py_estimate_size(),\n                    Self::Module(m) => m.py_estimate_size(),\n                    Self::Coroutine(coro) => coro.py_estimate_size(),\n                    Self::GatherFuture(gather) => gather.py_estimate_size(),\n                    Self::Path(p) => p.py_estimate_size(),\n                    Self::ReMatch(m) => m.py_estimate_size(),\n                    Self::RePattern(p) => p.py_estimate_size(),\n                    Self::ExtFunction(s) => std::mem::size_of::<String>() + s.len(),\n                }\n            }\n\n            fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n                match self {\n                    Self::Str(s) => s.py_dec_ref_ids(stack),\n                    Self::Bytes(b) => b.py_dec_ref_ids(stack),\n                    Self::List(l) => l.py_dec_ref_ids(stack),\n                    Self::Tuple(t) => t.py_dec_ref_ids(stack),\n                    Self::NamedTuple(nt) => nt.py_dec_ref_ids(stack),\n                    Self::Dict(d) => d.py_dec_ref_ids(stack),\n                    Self::DictKeysView(view) => view.py_dec_ref_ids(stack),\n                    Self::DictItemsView(view) => view.py_dec_ref_ids(stack),\n                    Self::DictValuesView(view) => view.py_dec_ref_ids(stack),\n                    Self::Set(s) => s.py_dec_ref_ids(stack),\n                    Self::FrozenSet(fs) => fs.py_dec_ref_ids(stack),\n                    Self::Closure(closure) => closure.py_dec_ref_ids(stack),\n                    Self::FunctionDefaults(fd) => fd.py_dec_ref_ids(stack),\n                    Self::Cell(cell) => cell.py_dec_ref_ids(stack),\n                    Self::Dataclass(dc) => dc.py_dec_ref_ids(stack),\n                    Self::Iter(iter) => iter.py_dec_ref_ids(stack),\n                    Self::Module(m) => m.py_dec_ref_ids(stack),\n                    Self::Coroutine(coro) => coro.py_dec_ref_ids(stack),\n                    Self::GatherFuture(gather) => gather.py_dec_ref_ids(stack),\n                    // Types with no nested heap references\n                    _ => {}\n                }\n            }\n        }\n    };\n}\n\nimpl_heap_item_dispatch!(HeapDataMut<'_>);\nimpl_heap_item_dispatch!(HeapData);\n"
  },
  {
    "path": "crates/monty/src/heap_traits.rs",
    "content": "use std::{mem::ManuallyDrop, ptr::addr_of};\n\nuse crate::{\n    ResourceTracker,\n    heap::{Heap, HeapId, RecursionToken},\n    value::Value,\n};\n\n/// Heap lifecycle operations for memory tracking and reference cleanup.\n///\n/// This trait captures the two responsibilities shared by all heap-stored types:\n///\n/// 1. **Memory estimation** (`py_estimate_size`): reporting approximate byte footprint\n///    for resource tracking and memory limit enforcement.\n///\n/// 2. **Reference collection** (`py_dec_ref_ids`): collecting contained `HeapId`s during\n///    reference count decrement so child objects can be freed iteratively.\n///\n/// Unlike `PyTrait`, which provides Python-level operations (equality, repr, arithmetic),\n/// `HeapItem` is purely about heap lifecycle management. This separation allows types like\n/// `Closure` and `FunctionDefaults` to participate in heap bookkeeping without needing\n/// the full `PyTrait` interface.\n///\n/// Every `HeapData` variant must implement this trait (either directly on the inner type,\n/// or inline in the dispatch for types we don't own like `String`).\npub(crate) trait HeapItem {\n    /// Estimates the memory size in bytes of this value.\n    ///\n    /// Used by resource tracking to enforce memory limits. Returns the approximate\n    /// heap footprint including struct overhead and variable-length data (e.g., string\n    /// contents, list elements).\n    ///\n    /// Note: For containers holding `Value::Ref` entries, this counts the size of\n    /// the reference slots, not the referenced objects. Nested objects are sized\n    /// separately when they are allocated.\n    fn py_estimate_size(&self) -> usize;\n\n    /// Pushes any contained `HeapId`s onto the stack for reference counting.\n    ///\n    /// This is called during `dec_ref` to find nested heap references that\n    /// need their refcounts decremented when this value is freed.\n    ///\n    /// When the `ref-count-panic` feature is enabled, this method also marks all\n    /// contained `Value`s as `Dereferenced` to prevent Drop panics. This\n    /// co-locates the cleanup logic with the reference collection logic.\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>);\n}\n\n/// This trait represents types that contain a `Heap`; it allows for more complex structures\n/// to participate in the `HeapGuard` pattern.\npub(crate) trait ContainsHeap {\n    type ResourceTracker: ResourceTracker;\n    fn heap(&self) -> &Heap<Self::ResourceTracker>;\n    fn heap_mut(&mut self) -> &mut Heap<Self::ResourceTracker>;\n}\n\nimpl<T: ResourceTracker> ContainsHeap for Heap<T> {\n    type ResourceTracker = T;\n    fn heap(&self) -> &Self {\n        self\n    }\n    #[inline]\n    fn heap_mut(&mut self) -> &mut Self {\n        self\n    }\n}\n\n/// Trait for types that require heap access for proper cleanup.\n///\n/// Rust's standard `Drop` trait cannot decrement heap reference counts because it has no\n/// access to the `Heap`. This trait provides an explicit drop-with-heap method so that\n/// ref-counted values (and containers of them) can properly decrement their counts when\n/// they are no longer needed.\n///\n/// **All types implementing this trait must be cleaned up on every code path** — not just\n/// the happy path, but also early returns, conditional branches, `continue`, etc. A missed\n/// call on any branch leaks reference counts. Prefer [`defer_drop!`] or [`HeapGuard`] to\n/// guarantee cleanup automatically rather than inserting manual calls in every branch.\n///\n/// Implemented for `Value`, `Option<V>`, `Vec<Value>`, `ArgValues`, iterators, and other\n/// types that hold heap references.\npub(crate) trait DropWithHeap: Sized {\n    /// Consume `self` and decrement reference counts for any heap-allocated values contained within.\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H);\n}\n\nimpl DropWithHeap for Value {\n    #[inline]\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        Self::drop_with_heap(self, heap);\n    }\n}\n\nimpl<U: DropWithHeap> DropWithHeap for Option<U> {\n    #[inline]\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        if let Some(value) = self {\n            value.drop_with_heap(heap);\n        }\n    }\n}\n\nimpl<U: DropWithHeap> DropWithHeap for Vec<U> {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        for value in self {\n            value.drop_with_heap(heap);\n        }\n    }\n}\n\nimpl<U: DropWithHeap> DropWithHeap for std::vec::IntoIter<U> {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        for value in self {\n            value.drop_with_heap(heap);\n        }\n    }\n}\n\nimpl<U: DropWithHeap> DropWithHeap for std::vec::Drain<'_, U> {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        for value in self {\n            value.drop_with_heap(heap);\n        }\n    }\n}\n\nimpl<const N: usize> DropWithHeap for [Value; N] {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        for value in self {\n            value.drop_with_heap(heap);\n        }\n    }\n}\n\nimpl<U: DropWithHeap, V: DropWithHeap> DropWithHeap for (U, V) {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        let (left, right) = self;\n        left.drop_with_heap(heap);\n        right.drop_with_heap(heap);\n    }\n}\n\n/// Trait for types that require only an immutable heap reference for cleanup.\n///\n/// Unlike [`DropWithHeap`], which requires `&mut Heap`, this trait works with `&Heap`.\n/// This is needed for cleanup in contexts that only have shared access to the heap,\n/// such as `py_repr_fmt` and `py_str` formatting methods.\n///\n/// Currently implemented for [`RecursionToken`], which decrements the recursion depth\n/// counter via interior mutability (`Cell`).\npub(crate) trait DropWithImmutableHeap {\n    /// Consume `self` and perform cleanup using an immutable heap reference.\n    fn drop_with_immutable_heap<T: ResourceTracker>(self, heap: &Heap<T>);\n}\n\nimpl DropWithImmutableHeap for RecursionToken {\n    #[inline]\n    fn drop_with_immutable_heap<T: ResourceTracker>(self, heap: &Heap<T>) {\n        heap.decr_recursion_depth();\n    }\n}\n\n/// RAII guard that ensures a [`DropWithImmutableHeap`] value is cleaned up on every code path.\n///\n/// Like [`HeapGuard`], but holds an immutable `&Heap<T>` instead of requiring `&mut` access\n/// via [`ContainsHeap`]. This is useful in contexts that only have shared access to the heap,\n/// such as `py_repr_fmt` formatting methods.\n///\n/// On the normal path, the guarded value can be borrowed via [`as_parts`](Self::as_parts).\n/// The guard's `Drop` impl calls [`DropWithImmutableHeap::drop_with_immutable_heap`]\n/// automatically, so cleanup happens on all exit paths.\npub(crate) struct ImmutableHeapGuard<'a, H: ContainsHeap, V: DropWithImmutableHeap> {\n    value: ManuallyDrop<V>,\n    heap: &'a H,\n}\n\nimpl<'a, H: ContainsHeap, V: DropWithImmutableHeap> ImmutableHeapGuard<'a, H, V> {\n    /// Creates a new `ImmutableHeapGuard` for the given value and immutable heap reference.\n    #[inline]\n    pub fn new(value: V, heap: &'a H) -> Self {\n        Self {\n            value: ManuallyDrop::new(value),\n            heap,\n        }\n    }\n\n    /// Borrows the value (immutably) and heap (immutably) out of the guard.\n    ///\n    /// This is what [`defer_drop_immutable_heap!`] calls internally. The returned\n    /// references are tied to the guard's lifetime, so the value cannot escape.\n    #[inline]\n    pub fn as_parts(&self) -> (&V, &'a H) {\n        (&self.value, self.heap)\n    }\n}\n\nimpl<H: ContainsHeap, V: DropWithImmutableHeap> Drop for ImmutableHeapGuard<'_, H, V> {\n    fn drop(&mut self) {\n        // SAFETY: [DH] - value is never manually dropped until this point\n        unsafe { ManuallyDrop::take(&mut self.value) }.drop_with_immutable_heap(self.heap.heap());\n    }\n}\n\n/// RAII guard that ensures a [`DropWithHeap`] value is cleaned up on every code path.\n///\n/// The guard's `Drop` impl calls [`DropWithHeap::drop_with_heap`] automatically, so\n/// cleanup happens whether the scope exits normally, via `?`, `continue`, early return,\n/// or any other branch. This eliminates the need to manually insert `drop_with_heap`\n/// calls in every branch.\n///\n/// On the normal path, the guarded value can be borrowed via [`as_parts`](Self::as_parts) /\n/// [`as_parts_mut`](Self::as_parts_mut), or reclaimed via [`into_inner`](Self::into_inner) /\n/// [`into_parts`](Self::into_parts) (which consume the guard without dropping the value).\n///\n/// Prefer the [`defer_drop!`] macro for the common case where you just need to ensure a\n/// value is dropped at scope exit. Use `HeapGuard` directly when you need to conditionally\n/// reclaim the value (e.g. push it back onto the stack on success) or need mutable access\n/// to both the value and heap through [`as_parts_mut`](Self::as_parts_mut).\npub(crate) struct HeapGuard<'a, H: ContainsHeap, V: DropWithHeap> {\n    // manually dropped because it needs to be dropped by move.\n    value: ManuallyDrop<V>,\n    heap: &'a mut H,\n}\n\nimpl<'a, H: ContainsHeap, V: DropWithHeap> HeapGuard<'a, H, V> {\n    /// Creates a new `HeapGuard` for the given value and heap.\n    #[inline]\n    pub fn new(value: V, heap: &'a mut H) -> Self {\n        Self {\n            value: ManuallyDrop::new(value),\n            heap,\n        }\n    }\n\n    /// Consumes the guard and returns the contained value without dropping it.\n    ///\n    /// Use this when the value should survive beyond the guard's scope (e.g. returning\n    /// a computed result from a function that used the guard for error-path safety).\n    #[inline]\n    pub fn into_inner(self) -> V {\n        let mut this = ManuallyDrop::new(self);\n        // SAFETY: [DH] - `ManuallyDrop::new(self)` prevents `Drop` on self, so we can take the value out\n        unsafe { ManuallyDrop::take(&mut this.value) }\n    }\n\n    /// Borrows the value (immutably) and heap (mutably) out of the guard.\n    ///\n    /// This is what [`defer_drop!`] calls internally. The returned references are tied\n    /// to the guard's lifetime, so the value cannot escape.\n    #[inline]\n    pub fn as_parts(&mut self) -> (&V, &mut H) {\n        (&self.value, self.heap)\n    }\n\n    /// Borrows the value (mutably) and heap (mutably) out of the guard.\n    ///\n    /// This is what [`defer_drop_mut!`] calls internally. Use this when the value needs\n    /// to be mutated in place (e.g. advancing an iterator, swapping during min/max).\n    #[inline]\n    pub fn as_parts_mut(&mut self) -> (&mut V, &mut H) {\n        (&mut self.value, self.heap)\n    }\n\n    /// Consumes the guard and returns the value and heap separately, without dropping.\n    ///\n    /// Use this when you need to reclaim both the value *and* the heap reference — for\n    /// example, to push the value back onto the VM stack via the heap owner.\n    #[inline]\n    pub fn into_parts(self) -> (V, &'a mut H) {\n        let mut this = ManuallyDrop::new(self);\n        // SAFETY: [DH] - `ManuallyDrop` prevents `Drop` on self, so we can recover the parts\n        unsafe { (ManuallyDrop::take(&mut this.value), addr_of!(this.heap).read()) }\n    }\n\n    /// Borrows just the heap out of the guard\n    #[inline]\n    pub fn heap(&mut self) -> &mut H {\n        self.heap\n    }\n}\n\nimpl<H: ContainsHeap, V: DropWithHeap> Drop for HeapGuard<'_, H, V> {\n    fn drop(&mut self) {\n        // SAFETY: [DH] - value is never manually dropped until this point\n        unsafe { ManuallyDrop::take(&mut self.value) }.drop_with_heap(self.heap.heap_mut());\n    }\n}\n\n/// The preferred way to ensure a [`DropWithHeap`] value is cleaned up on every code path.\n///\n/// Creates a [`HeapGuard`] and immediately rebinds `$value` as `&V` and `$heap` as\n/// `&mut H` via [`HeapGuard::as_parts`]. The original owned value is moved into the\n/// guard, which will call [`DropWithHeap::drop_with_heap`] when scope exits — whether\n/// that's normal completion, early return via `?`, `continue`, or any other branch.\n///\n/// Beyond safety, this is often much more concise than inserting `drop_with_heap` calls\n/// in every branch of complex control flow. For mutable access to the value, use\n/// [`defer_drop_mut!`].\n///\n/// # Limitation\n///\n/// The macro rebinds `$heap` as a new `let` binding, so it cannot be used when `$heap`\n/// is `self`. In `&mut self` methods, first assign `let this = self;` and pass `this`.\n#[macro_export]\nmacro_rules! defer_drop {\n    ($value:ident, $heap:ident) => {\n        let mut _guard = $crate::heap::HeapGuard::new($value, $heap);\n        #[allow(\n            clippy::allow_attributes,\n            reason = \"the reborrowed parts may not both be used in every case, so allow unused vars to avoid warnings\"\n        )]\n        #[allow(unused_variables)]\n        let ($value, $heap) = _guard.as_parts();\n    };\n}\n\n/// Like [`defer_drop!`], but rebinds `$value` as `&mut V` via [`HeapGuard::as_parts_mut`].\n///\n/// Use this when the value needs to be mutated in place — for example, advancing an\n/// iterator with `for_next()`, or swapping values during a min/max comparison.\n#[macro_export]\nmacro_rules! defer_drop_mut {\n    ($value:ident, $heap:ident) => {\n        let mut _guard = $crate::heap::HeapGuard::new($value, $heap);\n        #[allow(\n            clippy::allow_attributes,\n            reason = \"the reborrowed parts may not both be used in every case, so allow unused vars to avoid warnings\"\n        )]\n        #[allow(unused_variables)]\n        let ($value, $heap) = _guard.as_parts_mut();\n    };\n}\n\n/// Like [`defer_drop!`], but for [`DropWithImmutableHeap`] values that only need `&Heap`\n/// for cleanup.\n///\n/// Creates an [`ImmutableHeapGuard`] and immediately rebinds `$value` as `&V` and `$heap`\n/// as `&Heap<T>`. The guard will call [`DropWithImmutableHeap::drop_with_immutable_heap`]\n/// when scope exits. Use this for values like [`RecursionToken`] in contexts that only have\n/// shared access to the heap (e.g., `py_repr_fmt` formatting methods).\n#[macro_export]\nmacro_rules! defer_drop_immutable_heap {\n    ($value:ident, $heap:ident) => {\n        let _guard = $crate::heap::ImmutableHeapGuard::new($value, $heap);\n        #[allow(\n            clippy::allow_attributes,\n            reason = \"the reborrowed parts may not both be used in every case, so allow unused vars to avoid warnings\"\n        )]\n        #[allow(unused_variables)]\n        let ($value, $heap) = _guard.as_parts();\n    };\n}\n"
  },
  {
    "path": "crates/monty/src/intern.rs",
    "content": "//! String, bytes, and long integer interning for efficient storage of literals and identifiers.\n//!\n//! This module provides interners that store unique strings, bytes, and long integers in vectors\n//! and return indices (`StringId`, `BytesId`, `LongIntId`) for efficient storage and comparison.\n//! This avoids the overhead of cloning strings or using atomic reference counting.\n//!\n//! The interners are populated during parsing and preparation, then owned by the `Executor`.\n//! During execution, lookups are needed only for error messages and repr output.\n//!\n//! StringIds are laid out as follows:\n//! * 0 to 128 - single character strings for all 128 ASCII characters\n//! * 1000 to count(StaticStrings) - strings StaticStrings\n//! * 10_000+ - strings interned per executor\n\nuse std::{str::FromStr, sync::LazyLock};\n\nuse ahash::AHashMap;\nuse num_bigint::BigInt;\nuse strum::{EnumString, FromRepr, IntoStaticStr};\n\nuse crate::{function::Function, value::Value};\n\n/// Index into the string interner's storage.\n///\n/// Uses `u32` to save space (4 bytes vs 8 bytes for `usize`). This limits us to\n/// ~4 billion unique interns, which is more than sufficient.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub struct StringId(u32);\n\nimpl StringId {\n    /// Creates a StringId from a raw index value.\n    ///\n    /// Used by the bytecode VM to reconstruct StringIds from operands stored\n    /// in bytecode. The caller is responsible for ensuring the index is valid.\n    #[inline]\n    pub fn from_index(index: u16) -> Self {\n        Self(u32::from(index))\n    }\n\n    /// Returns the raw index value.\n    #[inline]\n    pub fn index(self) -> usize {\n        self.0 as usize\n    }\n\n    /// Returns the StringId for an ASCII byte.\n    #[must_use]\n    pub fn from_ascii(byte: u8) -> Self {\n        Self(u32::from(byte))\n    }\n}\n\n/// StringId offsets\nconst STATIC_STRING_ID_OFFSET: u32 = 1000;\nconst INTERN_STRING_ID_OFFSET: usize = 10_000;\n\n/// Static strings for all 128 ASCII characters, built once on first access.\n///\n/// Uses `LazyLock` to build the array at runtime (once), leaking the strings to get\n/// `'static` lifetime. The leak is intentional and bounded (128 single-byte strings).\nstatic ASCII_STRS: LazyLock<[&'static str; 128]> = LazyLock::new(|| {\n    std::array::from_fn(|i| {\n        // Safe: i is always 0-127 for a 128-element array\n        let s = char::from(u8::try_from(i).expect(\"index out of u8 range\")).to_string();\n        // Leak to get 'static lifetime - this is intentional and bounded (128 bytes total)\n        // Reborrow as immutable since we won't mutate\n        &*Box::leak(s.into_boxed_str())\n    })\n});\n\n/// Static string values which are known at compile time and don't need to be interned.\n#[repr(u8)]\n#[derive(\n    Debug, Clone, Copy, FromRepr, EnumString, IntoStaticStr, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,\n)]\n#[strum(serialize_all = \"snake_case\")]\npub enum StaticStrings {\n    #[strum(serialize = \"\")]\n    EmptyString,\n    #[strum(serialize = \"<module>\")]\n    Module,\n    // ==========================\n    // List methods\n    // Also uses shared: POP, CLEAR, COPY, REMOVE\n    // Also uses string-shared: INDEX, COUNT\n    Append,\n    Insert,\n    Extend,\n    Reverse,\n    Sort,\n\n    // ==========================\n    // Dict methods\n    // Also uses shared: POP, CLEAR, COPY, UPDATE\n    Get,\n    Keys,\n    Values,\n    Items,\n    Setdefault,\n    Popitem,\n    Fromkeys,\n\n    // ==========================\n    // Shared methods\n    // Used by multiple container types: list, dict, set\n    Pop,\n    Clear,\n    Copy,\n\n    // ==========================\n    // Set methods\n    // Also uses shared: POP, CLEAR, COPY\n    Add,\n    Remove,\n    Discard,\n    Update,\n    Union,\n    Intersection,\n    Difference,\n    SymmetricDifference,\n    Issubset,\n    Issuperset,\n    Isdisjoint,\n\n    // ==========================\n    // String methods\n    // Some methods shared with bytes: FIND, INDEX, COUNT, STARTSWITH, ENDSWITH\n    // Some methods shared with list/tuple: INDEX, COUNT\n    Join,\n    // Simple transformations\n    Lower,\n    Upper,\n    Capitalize,\n    Title,\n    Swapcase,\n    Casefold,\n    // Predicate methods\n    Isalpha,\n    Isdigit,\n    Isalnum,\n    Isnumeric,\n    Isspace,\n    Islower,\n    Isupper,\n    Isascii,\n    Isdecimal,\n    // Search methods (some shared with bytes, list, tuple)\n    Find,\n    Rfind,\n    Index,\n    Rindex,\n    Count,\n    Startswith,\n    Endswith,\n    // Strip/trim methods\n    Strip,\n    Lstrip,\n    Rstrip,\n    Removeprefix,\n    Removesuffix,\n    // Split methods\n    Split,\n    Rsplit,\n    Splitlines,\n    Partition,\n    Rpartition,\n    // Replace/padding methods\n    Replace,\n    Center,\n    Ljust,\n    Rjust,\n    Zfill,\n    // Additional string methods\n    Encode,\n    Isidentifier,\n    Istitle,\n\n    // ==========================\n    // Bytes methods\n    // Also uses string-shared: FIND, INDEX, COUNT, STARTSWITH, ENDSWITH\n    // Also uses most string methods: LOWER, UPPER, CAPITALIZE, TITLE, SWAPCASE,\n    // ISALPHA, ISDIGIT, ISALNUM, ISSPACE, ISLOWER, ISUPPER, ISASCII, ISTITLE,\n    // RFIND, RINDEX, STRIP, LSTRIP, RSTRIP, REMOVEPREFIX, REMOVESUFFIX,\n    // SPLIT, RSPLIT, SPLITLINES, PARTITION, RPARTITION, REPLACE,\n    // CENTER, LJUST, RJUST, ZFILL, JOIN\n    Decode,\n    Hex,\n    Fromhex,\n\n    // ==========================\n    // sys module strings\n    Sys,\n    #[strum(serialize = \"sys.version_info\")]\n    SysVersionInfo,\n    Version,\n    VersionInfo,\n    Platform,\n    Stdout,\n    Stderr,\n    Major,\n    Minor,\n    Micro,\n    Releaselevel,\n    Serial,\n    Final,\n    #[strum(serialize = \"3.14.0 (Monty)\")]\n    MontyVersionString,\n    Monty,\n\n    // ==========================\n    // os.stat_result fields\n    #[strum(serialize = \"StatResult\")]\n    OsStatResult,\n    StMode,\n    StIno,\n    StDev,\n    StNlink,\n    StUid,\n    StGid,\n    StSize,\n    StAtime,\n    StMtime,\n    StCtime,\n\n    // ==========================\n    // typing module strings\n    Typing,\n    #[strum(serialize = \"TYPE_CHECKING\")]\n    TypeChecking,\n    #[strum(serialize = \"Any\")]\n    Any,\n    #[strum(serialize = \"Optional\")]\n    Optional,\n    #[strum(serialize = \"Union\")]\n    UnionType,\n    #[strum(serialize = \"List\")]\n    ListType,\n    #[strum(serialize = \"Dict\")]\n    DictType,\n    #[strum(serialize = \"Tuple\")]\n    TupleType,\n    #[strum(serialize = \"Set\")]\n    SetType,\n    #[strum(serialize = \"FrozenSet\")]\n    FrozenSet,\n    #[strum(serialize = \"Callable\")]\n    Callable,\n    #[strum(serialize = \"Type\")]\n    Type,\n    #[strum(serialize = \"Sequence\")]\n    Sequence,\n    #[strum(serialize = \"Mapping\")]\n    Mapping,\n    #[strum(serialize = \"Iterable\")]\n    Iterable,\n    #[strum(serialize = \"Iterator\")]\n    IteratorType,\n    #[strum(serialize = \"Generator\")]\n    Generator,\n    #[strum(serialize = \"ClassVar\")]\n    ClassVar,\n    #[strum(serialize = \"Final\")]\n    FinalType,\n    #[strum(serialize = \"Literal\")]\n    Literal,\n    #[strum(serialize = \"TypeVar\")]\n    TypeVar,\n    #[strum(serialize = \"Generic\")]\n    Generic,\n    #[strum(serialize = \"Protocol\")]\n    Protocol,\n    #[strum(serialize = \"Annotated\")]\n    Annotated,\n    #[strum(serialize = \"Self\")]\n    SelfType,\n    #[strum(serialize = \"Never\")]\n    Never,\n    #[strum(serialize = \"NoReturn\")]\n    NoReturn,\n\n    // ==========================\n    // asyncio module strings\n    Asyncio,\n    Gather,\n    Run,\n\n    // ==========================\n    // os module strings\n    Os,\n    Getenv,\n    Environ,\n    Default,\n\n    // ==========================\n    // Exception attributes\n    Args,\n\n    // ==========================\n    // Type attributes\n    #[strum(serialize = \"__name__\")]\n    DunderName,\n\n    // ==========================\n    // pathlib module strings\n    Pathlib,\n    #[strum(serialize = \"Path\")]\n    PathClass,\n\n    // Path properties (pure - no I/O)\n    Name,\n    Parent,\n    Stem,\n    Suffix,\n    Suffixes,\n    Parts,\n\n    // Path pure methods (no I/O)\n    IsAbsolute,\n    Joinpath,\n    WithName,\n    WithStem,\n    WithSuffix,\n    AsPosix,\n    #[strum(serialize = \"__fspath__\")]\n    Fspath,\n\n    // Path filesystem methods (require OsAccess - yield external calls)\n    Exists,\n    IsFile,\n    IsDir,\n    IsSymlink,\n    #[strum(serialize = \"stat\")]\n    StatMethod,\n    ReadBytes,\n    ReadText,\n    Iterdir,\n    Resolve,\n    Absolute,\n\n    // Path write methods (require OsAccess - yield external calls)\n    WriteText,\n    WriteBytes,\n    Mkdir,\n    Unlink,\n    Rmdir,\n    Rename,\n\n    // Slice attributes\n    Start,\n    Stop,\n    Step,\n\n    // ==========================\n    // module strings\n    // ==========================\n\n    // math module strings\n    Math,\n    // Rounding\n    Floor,\n    Ceil,\n    Trunc,\n    // Roots & powers\n    Sqrt,\n    Isqrt,\n    Cbrt,\n    Pow,\n    Exp,\n    Exp2,\n    Expm1,\n    // Logarithms\n    Log,\n    Log1p,\n    Log2,\n    Log10,\n    // Float properties\n    Fabs,\n    Isnan,\n    Isinf,\n    Isfinite,\n    Copysign,\n    Isclose,\n    Nextafter,\n    Ulp,\n    // Trigonometric\n    Sin,\n    Cos,\n    Tan,\n    Asin,\n    Acos,\n    Atan,\n    Atan2,\n    // Hyperbolic\n    Sinh,\n    Cosh,\n    Tanh,\n    Asinh,\n    Acosh,\n    Atanh,\n    // Angular conversion\n    Degrees,\n    Radians,\n    // Integer math\n    Factorial,\n    Gcd,\n    Lcm,\n    Comb,\n    Perm,\n    // Modular / decomposition\n    Fmod,\n    Remainder,\n    Modf,\n    Frexp,\n    Ldexp,\n    // Special functions\n    Gamma,\n    Lgamma,\n    Erf,\n    Erfc,\n    // Constants\n    /// `math.pi` constant\n    Pi,\n    /// `math.e` constant\n    #[strum(serialize = \"e\")]\n    MathE,\n    /// `math.tau` constant\n    Tau,\n    /// `math.inf` constant\n    #[strum(serialize = \"inf\")]\n    MathInf,\n    /// `math.nan` constant\n    #[strum(serialize = \"nan\")]\n    MathNan,\n\n    // re module strings\n    /// Module name for `import re`.\n    Re,\n    /// `re.compile()` function\n    Compile,\n    /// `re.match()` / `pattern.match()` method\n    Match,\n    /// `re.search()` / `pattern.search()` method\n    Search,\n    /// `re.fullmatch()` / `pattern.fullmatch()` method\n    Fullmatch,\n    /// `re.findall()` / `pattern.findall()` method\n    Findall,\n    /// `re.sub()` / `pattern.sub()` method\n    Sub,\n    /// `match.group()` method\n    Group,\n    /// `match.groups()` method\n    Groups,\n    /// `match.span()` method\n    Span,\n    /// `match.end()` method\n    End,\n    /// `re.Pattern`\n    #[strum(serialize = \"Pattern\")]\n    PatternClass,\n    /// `re.Match`\n    #[strum(serialize = \"Match\")]\n    MatchClass,\n    /// `pattern.pattern`\n    #[strum(serialize = \"pattern\")]\n    PatternAttr,\n    /// `match.string`\n    #[strum(serialize = \"string\")]\n    StringAttr,\n    /// `pattern.flags`\n    Flags,\n    /// `re.IGNORECASE` flag\n    #[strum(serialize = \"IGNORECASE\")]\n    Ignorecase,\n    /// `re.I` flag, alias\n    #[strum(serialize = \"I\")]\n    I,\n    /// `re.MULTILINE` flag\n    #[strum(serialize = \"MULTILINE\")]\n    MultilineFlag,\n    /// `re.M` flag, alias\n    #[strum(serialize = \"M\")]\n    M,\n    /// `re.DOTALL` flag\n    #[strum(serialize = \"DOTALL\")]\n    DotallFlag,\n    /// `re.S` flag, alias\n    #[strum(serialize = \"S\")]\n    S,\n    /// `re.NOFLAG` flag\n    #[strum(serialize = \"NOFLAG\")]\n    NoFlag,\n    /// `re.ASCII` flag\n    #[strum(serialize = \"ASCII\")]\n    AsciiFlag,\n    /// `re.A` flag, alias\n    #[strum(serialize = \"A\")]\n    A,\n    /// `re.PatternError` exception\n    #[strum(serialize = \"PatternError\")]\n    PatternError,\n    /// `re.error` exception alias (same as `re.PatternError`)\n    #[strum(serialize = \"error\")]\n    Error,\n    /// `re.escape()` function\n    Escape,\n    /// `re.finditer()` / `pattern.finditer()` method\n    Finditer,\n    /// `match.groupdict()` method\n    Groupdict,\n}\n\nimpl StaticStrings {\n    /// Attempts to convert a `StringId` back to a `StaticStrings` variant.\n    ///\n    /// Returns `None` if the `StringId` doesn't correspond to a static string\n    /// (e.g., it's an ASCII char or a dynamically interned string).\n    pub fn from_string_id(id: StringId) -> Option<Self> {\n        let enum_id = id.0.checked_sub(STATIC_STRING_ID_OFFSET)?;\n        u8::try_from(enum_id).ok().and_then(Self::from_repr)\n    }\n}\n\n/// Converts this static string variant to its corresponding `StringId`.\nimpl From<StaticStrings> for StringId {\n    fn from(value: StaticStrings) -> Self {\n        let string_id = value as u32;\n        Self(string_id + STATIC_STRING_ID_OFFSET)\n    }\n}\n\nimpl From<StaticStrings> for Value {\n    fn from(value: StaticStrings) -> Self {\n        Self::InternString(value.into())\n    }\n}\n\nimpl PartialEq<StaticStrings> for StringId {\n    fn eq(&self, other: &StaticStrings) -> bool {\n        *self == Self::from(*other)\n    }\n}\n\nimpl PartialEq<StringId> for StaticStrings {\n    fn eq(&self, other: &StringId) -> bool {\n        StringId::from(*self) == *other\n    }\n}\n\n/// Index into the bytes interner's storage.\n///\n/// Separate from `StringId` to distinguish string vs bytes literals at the type level.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub struct BytesId(u32);\n\nimpl BytesId {\n    /// Returns the raw index value.\n    #[inline]\n    pub fn index(self) -> usize {\n        self.0 as usize\n    }\n}\n\n/// Index into the long integer interner's storage.\n///\n/// Used for integer literals that exceed i64 range. The actual `BigInt` values\n/// are stored in the `Interns` table and looked up by index at runtime.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub struct LongIntId(u32);\n\nimpl LongIntId {\n    /// Returns the raw index value.\n    #[inline]\n    pub fn index(self) -> usize {\n        self.0 as usize\n    }\n}\n\n/// Unique identifier for functions\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]\npub struct FunctionId(u32);\n\nimpl FunctionId {\n    /// Creates a FunctionId from a raw index value.\n    ///\n    /// Used by the bytecode VM to reconstruct FunctionIds from operands stored\n    /// in bytecode. The caller is responsible for ensuring the index is valid.\n    #[inline]\n    pub fn from_index(index: u16) -> Self {\n        Self(u32::from(index))\n    }\n\n    /// Returns the raw index value.\n    #[inline]\n    pub fn index(self) -> usize {\n        self.0 as usize\n    }\n}\n\n/// A string, bytes, and long integer interner that stores unique values and returns indices for lookup.\n///\n/// Interns are deduplicated on insertion - interning the same string twice returns\n/// the same `StringId`. Bytes and long integers are NOT deduplicated (rare enough that it's not worth it).\n/// The interner owns all strings/bytes/long integers and provides lookup by index.\n///\n/// # Thread Safety\n///\n/// The interner is not thread-safe. It's designed to be used single-threaded during\n/// parsing/preparation, then the values are accessed read-only during execution.\n#[derive(Debug, Default, Clone)]\npub struct InternerBuilder {\n    /// Maps strings to their indices for deduplication during interning.\n    string_map: AHashMap<String, StringId>,\n    /// Storage for interned interns, indexed by `StringId`.\n    strings: Vec<String>,\n    /// Storage for interned bytes literals, indexed by `BytesId`.\n    /// Not deduplicated since bytes literals are rare.\n    bytes: Vec<Vec<u8>>,\n    /// Storage for interned long integer literals, indexed by `LongIntId`.\n    /// Not deduplicated since long integer literals are rare.\n    long_ints: Vec<BigInt>,\n}\n\nimpl InternerBuilder {\n    /// Creates a new string interner with pre-interned strings.\n    ///\n    /// Clones from a lazily-initialized base interner that contains all pre-interned\n    /// strings (`<module>`, attribute names, ASCII chars). This avoids rebuilding\n    /// the base set on every call.\n    ///\n    /// # Arguments\n    /// * `code` - The code being parsed, used for a very rough guess at how many\n    ///   additional strings will be interned beyond the base set.\n    ///\n    /// Pre-interns (via `BASE_INTERNER`):\n    /// - Index 0: `\"<module>\"` for module-level code\n    /// - Indices 1-MAX_ATTR_ID: Known attribute names (append, insert, get, join, etc.)\n    /// - Indices MAX_ATTR_ID+1..: ASCII single-character strings\n    pub fn new(code: &str) -> Self {\n        // Reserve capacity for code-specific strings\n        // Rough guess: count quotes and divide by 2 (open+close per string)\n        let capacity = code.bytes().filter(|&b| b == b'\"' || b == b'\\'').count() >> 1;\n        Self {\n            string_map: AHashMap::with_capacity(capacity),\n            strings: Vec::with_capacity(capacity),\n            bytes: Vec::new(),\n            long_ints: Vec::new(),\n        }\n    }\n\n    /// Creates a builder pre-seeded from an existing [`Interns`] table.\n    ///\n    /// This is used by REPL incremental compilation: previously compiled interned\n    /// values keep stable IDs, and newly interned values are appended.\n    pub(crate) fn from_interns(interns: &Interns, code: &str) -> Self {\n        let mut builder = Self::new(code);\n        builder.strings.clone_from(&interns.strings);\n        builder.bytes.clone_from(&interns.bytes);\n        builder.long_ints.clone_from(&interns.long_ints);\n\n        builder.string_map = builder\n            .strings\n            .iter()\n            .enumerate()\n            .map(|(index, value)| {\n                let id = StringId(\n                    u32::try_from(INTERN_STRING_ID_OFFSET + index).expect(\"StringId overflow while seeding interner\"),\n                );\n                (value.clone(), id)\n            })\n            .collect();\n        builder\n    }\n\n    /// Interns a string, returning its `StringId`.\n    ///\n    /// * If the string is ascii, return the pre-interned string id\n    /// * If the string is a known static string, return the pre-interned string id\n    /// * If the string was already interned, returns the existing string id\n    /// * Otherwise, stores the string and returns a new string id\n    pub fn intern(&mut self, s: &str) -> StringId {\n        if s.len() == 1 {\n            StringId::from_ascii(s.as_bytes()[0])\n        } else if let Ok(ss) = StaticStrings::from_str(s) {\n            ss.into()\n        } else {\n            *self.string_map.entry(s.to_owned()).or_insert_with(|| {\n                let string_id = self.strings.len() + INTERN_STRING_ID_OFFSET;\n                let id = StringId(string_id.try_into().expect(\"StringId overflow\"));\n                self.strings.push(s.to_owned());\n                id\n            })\n        }\n    }\n\n    /// Interns bytes, returning its `BytesId`.\n    ///\n    /// Unlike interns, bytes are not deduplicated (bytes literals are rare).\n    pub fn intern_bytes(&mut self, b: &[u8]) -> BytesId {\n        let id = BytesId(self.bytes.len().try_into().expect(\"BytesId overflow\"));\n        self.bytes.push(b.to_vec());\n        id\n    }\n\n    /// Interns a long integer, returning its `LongIntId`.\n    ///\n    /// Big integers are not deduplicated since literals exceeding i64 are rare.\n    pub fn intern_long_int(&mut self, bi: BigInt) -> LongIntId {\n        let id = LongIntId(self.long_ints.len().try_into().expect(\"LongIntId overflow\"));\n        self.long_ints.push(bi);\n        id\n    }\n\n    /// Looks up a string by its `StringId`.\n    #[inline]\n    pub fn get_str(&self, id: StringId) -> &str {\n        get_str(&self.strings, id)\n    }\n}\n\n/// Looks up a string by its `StringId`.\n///\n/// # Panics\n///\n/// Panics if the `StringId` is invalid - not from this interner or ascii chars or StaticStrings.\nfn get_str(strings: &[String], id: StringId) -> &str {\n    if let Ok(c) = u8::try_from(id.0) {\n        ASCII_STRS[c as usize]\n    } else if let Some(intern_index) = id.index().checked_sub(INTERN_STRING_ID_OFFSET) {\n        &strings[intern_index]\n    } else {\n        let static_str = StaticStrings::from_string_id(id).expect(\"Invalid static string ID\");\n        static_str.into()\n    }\n}\n\n/// Read-only storage for interned strings, bytes, and long integers.\n///\n/// This provides lookup by `StringId`, `BytesId`, `LongIntId` and `FunctionId` for interned literals and functions.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) struct Interns {\n    strings: Vec<String>,\n    bytes: Vec<Vec<u8>>,\n    long_ints: Vec<BigInt>,\n    functions: Vec<Function>,\n}\n\nimpl Interns {\n    pub fn new(interner: InternerBuilder, functions: Vec<Function>) -> Self {\n        Self {\n            strings: interner.strings,\n            bytes: interner.bytes,\n            long_ints: interner.long_ints,\n            functions,\n        }\n    }\n\n    /// Looks up a string by its `StringId`.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the `StringId` is invalid.\n    #[inline]\n    pub fn get_str(&self, id: StringId) -> &str {\n        get_str(&self.strings, id)\n    }\n\n    /// Looks up bytes by their `BytesId`.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the `BytesId` is invalid.\n    #[inline]\n    pub fn get_bytes(&self, id: BytesId) -> &[u8] {\n        &self.bytes[id.index()]\n    }\n\n    /// Looks up a long integer by its `LongIntId`.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the `LongIntId` is invalid.\n    #[inline]\n    pub fn get_long_int(&self, id: LongIntId) -> &BigInt {\n        &self.long_ints[id.index()]\n    }\n\n    /// Lookup a function by its `FunctionId`\n    ///\n    /// # Panics\n    ///\n    /// Panics if the `FunctionId` is invalid.\n    #[inline]\n    pub fn get_function(&self, id: FunctionId) -> &Function {\n        self.functions.get(id.index()).expect(\"Function not found\")\n    }\n\n    /// Looks up the `StringId` for a string, checking ASCII, static strings, and interned strings.\n    ///\n    /// This is the reverse of `get_str`: given a string, find its StringId.\n    /// Used when the host provides a name (e.g., from a NameLookup response) that was\n    /// previously interned during preparation.\n    ///\n    /// Error if the string was never interned.\n    pub fn get_string_id_by_name(&self, s: &str) -> Option<StringId> {\n        // Check single ASCII char\n        if s.len() == 1 {\n            return Some(StringId::from_ascii(s.as_bytes()[0]));\n        }\n        // Check static strings\n        if let Ok(ss) = StaticStrings::from_str(s) {\n            return Some(ss.into());\n        }\n        // Check interned strings\n        for (i, interned) in self.strings.iter().enumerate() {\n            if interned == s {\n                return u32::try_from(INTERN_STRING_ID_OFFSET + i).ok().map(StringId);\n            }\n        }\n        None\n    }\n\n    /// Sets the compiled functions.\n    ///\n    /// This is called after compilation to populate the functions that were\n    /// compiled from `PreparedFunctionDef` nodes.\n    pub fn set_functions(&mut self, functions: Vec<Function>) {\n        self.functions = functions;\n    }\n\n    /// Returns a clone of the compiled function table.\n    ///\n    /// Used by REPL incremental compilation to preserve existing function IDs.\n    pub(crate) fn functions_clone(&self) -> Vec<Function> {\n        self.functions.clone()\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/io.rs",
    "content": "use std::borrow::Cow;\n\nuse crate::exception_public::MontyException;\n\n/// Output handler for the `print()` builtin function.\n///\n/// Provides common output modes as enum variants to avoid trait object overhead\n/// in the typical cases (stdout, disabled, collect). For custom output handling,\n/// use the `Callback` variant with a [`PrintWriterCallback`] implementation.\n///\n/// # Variants\n/// - `Disabled` - Silently discards all output (useful for benchmarking or suppressing output)\n/// - `Stdout` - Writes to standard output (the default behavior)\n/// - `Collect` - Accumulates output into a target `String` for programmatic access\n/// - `Callback` - Delegates to a user-provided [`PrintWriterCallback`] implementation\npub enum PrintWriter<'a> {\n    /// Silently discard all output.\n    Disabled,\n    /// Write to standard output.\n    Stdout,\n    /// Collect all output into a string.\n    Collect(&'a mut String),\n    /// Delegate to a custom callback.\n    Callback(&'a mut dyn PrintWriterCallback),\n}\n\nimpl PrintWriter<'_> {\n    /// Creates a new `PrintWriter` that reborrows the same underlying target.\n    ///\n    /// This is useful in iterative execution (`start`/`resume` loops) where each\n    /// step takes `PrintWriter` by value but you want all steps to write to the\n    /// same output target. The original writer remains valid after the reborrowed\n    /// copy is dropped.\n    pub fn reborrow(&mut self) -> PrintWriter<'_> {\n        match self {\n            Self::Disabled => PrintWriter::Disabled,\n            Self::Stdout => PrintWriter::Stdout,\n            Self::Collect(buf) => PrintWriter::Collect(buf),\n            Self::Callback(cb) => PrintWriter::Callback(&mut **cb),\n        }\n    }\n\n    /// Called once for each formatted argument passed to `print()`.\n    ///\n    /// This method writes only the given argument's text, without adding\n    /// separators or a trailing newline. Separators (spaces) and the final\n    /// terminator (newline) are emitted via [`stdout_push`](Self::stdout_push).\n    pub fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException> {\n        match self {\n            Self::Disabled => Ok(()),\n            Self::Stdout => {\n                print!(\"{output}\");\n                Ok(())\n            }\n            Self::Collect(buf) => {\n                buf.push_str(&output);\n                Ok(())\n            }\n            Self::Callback(cb) => cb.stdout_write(output),\n        }\n    }\n\n    /// Appends a single character to the output.\n    ///\n    /// Generally called to add spaces (separators) and newlines (terminators)\n    /// within print output.\n    pub fn stdout_push(&mut self, end: char) -> Result<(), MontyException> {\n        match self {\n            Self::Disabled => Ok(()),\n            Self::Stdout => {\n                print!(\"{end}\");\n                Ok(())\n            }\n            Self::Collect(buf) => {\n                buf.push(end);\n                Ok(())\n            }\n            Self::Callback(cb) => cb.stdout_push(end),\n        }\n    }\n}\n\n/// Trait for custom output handling from the `print()` builtin function.\n///\n/// Implement this trait and pass it via [`PrintWriter::Callback`] to capture\n/// or redirect print output from sandboxed Python code.\npub trait PrintWriterCallback {\n    /// Called once for each formatted argument passed to `print()`.\n    ///\n    /// This method is responsible for writing only the given argument's text, and must\n    /// not add separators or a trailing newline. Separators (such as spaces) and the\n    /// final terminator (such as a newline) are emitted via [`stdout_push`](Self::stdout_push).\n    ///\n    /// # Arguments\n    /// * `output` - The formatted output string for a single argument (without\n    ///   separators or trailing newline).\n    fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException>;\n\n    /// Add a single character to stdout.\n    ///\n    /// Generally called to add spaces and newlines within print output.\n    ///\n    /// # Arguments\n    /// * `end` - The character to print after the formatted output.\n    fn stdout_push(&mut self, end: char) -> Result<(), MontyException>;\n}\n"
  },
  {
    "path": "crates/monty/src/lib.rs",
    "content": "#![doc = include_str!(\"../../../README.md\")]\n// first to include defer_drop macro\nmod heap_traits;\n\nmod args;\nmod asyncio;\nmod builtins;\nmod bytecode;\nmod exception_private;\nmod exception_public;\nmod expressions;\nmod fstring;\nmod function;\nmod heap;\nmod heap_data;\nmod intern;\nmod io;\nmod modules;\nmod namespace;\nmod object;\nmod os;\nmod parse;\nmod prepare;\nmod repl;\nmod resource;\nmod run;\nmod run_progress;\nmod signature;\nmod sorting;\nmod types;\nmod value;\n\n#[cfg(feature = \"ref-count-return\")]\npub use crate::run::RefCountOutput;\npub use crate::{\n    exception_private::ExcType,\n    exception_public::{CodeLoc, MontyException, StackFrame},\n    io::{PrintWriter, PrintWriterCallback},\n    object::{DictPairs, InvalidInputError, MontyObject},\n    os::{OsFunction, dir_stat, file_stat, stat_result, symlink_stat},\n    repl::{\n        MontyRepl, ReplContinuationMode, ReplFunctionCall, ReplNameLookup, ReplOsCall, ReplProgress,\n        ReplResolveFutures, ReplStartError, detect_repl_continuation_mode,\n    },\n    resource::{\n        DEFAULT_MAX_RECURSION_DEPTH, LimitedTracker, NoLimitTracker, ResourceError, ResourceLimits, ResourceTracker,\n    },\n    run::MontyRun,\n    run_progress::{\n        ExtFunctionResult, FunctionCall, NameLookup, NameLookupResult, OsCall, ResolveFutures, RunProgress,\n    },\n};\n"
  },
  {
    "path": "crates/monty/src/modules/asyncio.rs",
    "content": "//! Implementation of the `asyncio` module.\n//!\n//! Provides a minimal implementation of Python's `asyncio` module with:\n//! - `run(coro)`: Runs a coroutine to completion, equivalent to `await coro`\n//! - `gather(*awaitables)`: Collects coroutines for concurrent execution\n//!\n//! Other asyncio functions (`create_task`, `sleep`, `wait`, etc.) are not implemented.\n//! The host acts as the event loop - Monty yields control when tasks are blocked.\n\nuse crate::{\n    args::ArgValues,\n    asyncio::{GatherFuture, GatherItem},\n    bytecode::{CallResult, VM},\n    defer_drop_mut,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapData, HeapId},\n    intern::StaticStrings,\n    modules::ModuleFunctions,\n    resource::{ResourceError, ResourceTracker},\n    types::Module,\n    value::Value,\n};\n\n/// Async Functions.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, serde::Serialize, serde::Deserialize)]\n#[strum(serialize_all = \"lowercase\")]\npub(crate) enum AsyncioFunctions {\n    Gather,\n    Run,\n}\n\n/// Creates the `asyncio` module and allocates it on the heap.\n///\n/// The module contains only the `gather` function. Other asyncio functions\n/// are not implemented as they would require additional VM/scheduler features.\n///\n/// # Returns\n/// A HeapId pointing to the newly allocated module.\n///\n/// # Panics\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Asyncio);\n\n    module.set_attr(\n        StaticStrings::Gather,\n        Value::ModuleFunction(ModuleFunctions::Asyncio(AsyncioFunctions::Gather)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Run,\n        Value::ModuleFunction(ModuleFunctions::Asyncio(AsyncioFunctions::Run)),\n        vm,\n    );\n\n    vm.heap.allocate(HeapData::Module(module))\n}\npub(super) fn call(\n    heap: &mut Heap<impl ResourceTracker>,\n    functions: AsyncioFunctions,\n    args: ArgValues,\n) -> RunResult<CallResult> {\n    match functions {\n        AsyncioFunctions::Gather => gather(heap, args).map(CallResult::Value),\n        AsyncioFunctions::Run => run(heap, args),\n    }\n}\n\n/// Implementation of `asyncio.run(coro)`.\n///\n/// Runs a single coroutine to completion, equivalent to `await coro` at the top level.\n/// Accepts exactly one positional argument (the coroutine) and no keyword arguments.\n///\n/// Returns `CallResult::AwaitValue` so the VM executes `exec_get_awaitable` on\n/// the value, which handles validation that it's actually a coroutine/awaitable.\nfn run(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {\n    let coroutine = args.get_one_arg(\"asyncio.run\", heap)?;\n    Ok(CallResult::AwaitValue(coroutine))\n}\n\n/// Implementation of `asyncio.gather(*awaitables)`.\n///\n/// Collects coroutines and external futures for concurrent execution. Does NOT\n/// spawn tasks immediately - just validates and stores the references. Tasks are\n/// spawned when the returned `GatherFuture` is awaited (in the `Await` opcode handler).\n///\n/// # Behavior when awaited\n///\n/// 1. Each coroutine is spawned as a separate Task\n/// 2. External futures are tracked for resolution by the host\n/// 3. The current task blocks until all items complete\n/// 4. Results are collected in order and returned as a list\n/// 5. On any task failure, sibling tasks are cancelled and the exception propagates\n///\n/// # Arguments\n/// * `heap` - The heap for allocating the GatherFuture\n/// * `args` - Variadic awaitable arguments (coroutines or external futures)\n///\n/// # Errors\n/// Returns `TypeError` if any argument is not awaitable.\npub(crate) fn gather(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pos_args, kwargs) = args.into_parts();\n    defer_drop_mut!(pos_args, heap);\n\n    // TODO: support keyword arguments (e.g. return_exceptions)\n    kwargs.not_supported_yet(\"gather\", heap)?;\n\n    // Validate all positional args are awaitable and collect them\n    let mut items = Vec::new();\n    let mut coroutine_ids_to_cleanup: Vec<HeapId> = Vec::new();\n\n    #[cfg_attr(not(feature = \"ref-count-panic\"), expect(unused_mut))]\n    for mut arg in pos_args {\n        match &arg {\n            Value::Ref(id) if heap.get(*id).is_coroutine() => {\n                coroutine_ids_to_cleanup.push(*id);\n                items.push(GatherItem::Coroutine(*id));\n                // Transfer ownership to GatherFuture - mark Value as consumed without dec_ref\n                #[cfg(feature = \"ref-count-panic\")]\n                arg.dec_ref_forget();\n            }\n            Value::ExternalFuture(call_id) => {\n                items.push(GatherItem::ExternalFuture(*call_id));\n                // ExternalFuture is Copy, no refcount to manage\n            }\n            _ => {\n                // Not awaitable - clean up and error\n                arg.drop_with_heap(heap);\n                // Drop already-collected coroutine refs\n                for cid in coroutine_ids_to_cleanup {\n                    heap.dec_ref(cid);\n                }\n                return Err(ExcType::type_error(\n                    \"An asyncio.Future, a coroutine or an awaitable is required\",\n                ));\n            }\n        }\n    }\n\n    // Create GatherFuture on heap\n    let gather_future = GatherFuture::new(items);\n    let id = heap.allocate(HeapData::GatherFuture(gather_future))?;\n    Ok(Value::Ref(id))\n}\n"
  },
  {
    "path": "crates/monty/src/modules/math.rs",
    "content": "//! Implementation of Python's `math` module.\n//!\n//! Provides mathematical functions and constants matching CPython 3.14 behavior\n//! and error messages. All functions are pure computations that don't require\n//! host involvement, so they return `Value` directly rather than `AttrCallResult`.\n//!\n//! ## Implemented functions\n//!\n//! **Rounding**: `floor`, `ceil`, `trunc`\n//! **Roots & powers**: `sqrt`, `isqrt`, `cbrt`, `pow`, `exp`, `exp2`, `expm1`\n//! **Logarithms**: `log`, `log2`, `log10`, `log1p`\n//! **Trigonometric**: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`\n//! **Hyperbolic**: `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`\n//! **Angular**: `degrees`, `radians`\n//! **Float properties**: `fabs`, `isnan`, `isinf`, `isfinite`, `copysign`, `isclose`,\n//!   `nextafter`, `ulp`\n//! **Integer math**: `factorial`, `gcd`, `lcm`, `comb`, `perm`\n//! **Modular**: `fmod`, `remainder`, `modf`, `frexp`, `ldexp`\n//! **Special**: `gamma`, `lgamma`, `erf`, `erfc`\n//!\n//! ## Constants\n//!\n//! `pi`, `e`, `tau`, `inf`, `nan`\n\nuse num_bigint::BigInt;\nuse smallvec::smallvec;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::{Heap, HeapData, HeapId},\n    intern::{Interns, StaticStrings},\n    modules::ModuleFunctions,\n    resource::{ResourceError, ResourceTracker},\n    types::{LongInt, Module, PyTrait, allocate_tuple},\n    value::Value,\n};\n\n// ==========================\n// Shared constants and error helpers\n// ==========================\n\n/// Returns a `ValueError` with the standard CPython \"math domain error\" message.\nfn math_domain_error() -> crate::exception_private::RunError {\n    SimpleException::new_msg(ExcType::ValueError, \"math domain error\").into()\n}\n\n/// Returns an `OverflowError` with the standard CPython \"math range error\" message.\nfn math_range_error() -> crate::exception_private::RunError {\n    SimpleException::new_msg(ExcType::OverflowError, \"math range error\").into()\n}\n\n/// Checks whether a computation overflowed (finite input produced infinite result).\n///\n/// Returns `Err(OverflowError(\"math range error\"))` if `result` is infinite\n/// but `input` was finite.\nfn check_range_error(result: f64, input: f64) -> RunResult<()> {\n    if result.is_infinite() && input.is_finite() {\n        Err(math_range_error())\n    } else {\n        Ok(())\n    }\n}\n\n/// Checks that a value is in the `[-1, 1]` range, raising `ValueError` if not.\n///\n/// NaN passes through (it will propagate through the subsequent math operation).\n/// Used by `math.asin` and `math.acos`.\nfn require_unit_range(f: f64) -> RunResult<()> {\n    if !f.is_nan() && !(-1.0..=1.0).contains(&f) {\n        Err(SimpleException::new_msg(\n            ExcType::ValueError,\n            format!(\"expected a number in range from -1 up to 1, got {f:?}\"),\n        )\n        .into())\n    } else {\n        Ok(())\n    }\n}\n\n/// Checks for non-positive integer arguments (poles of the Gamma function).\n///\n/// These are the finite non-positive integers where Gamma diverges to ±∞.\n/// Does NOT reject `-inf` — callers that need to reject it (like `math.gamma`)\n/// must do so separately, since `lgamma(-inf)` is valid and returns `inf`.\n#[expect(\n    clippy::float_cmp,\n    reason = \"exact comparison detects integer poles of gamma function\"\n)]\nfn check_gamma_pole(f: f64) -> RunResult<()> {\n    if f <= 0.0 && f == f.floor() && f.is_finite() {\n        Err(SimpleException::new_msg(\n            ExcType::ValueError,\n            format!(\"expected a noninteger or positive integer, got {f:?}\"),\n        )\n        .into())\n    } else {\n        Ok(())\n    }\n}\n\n/// Math module functions — each variant corresponds to a Python-visible function.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, serde::Serialize, serde::Deserialize)]\n#[strum(serialize_all = \"lowercase\")]\npub(crate) enum MathFunctions {\n    // Rounding\n    Floor,\n    Ceil,\n    Trunc,\n    // Roots & powers\n    Sqrt,\n    Isqrt,\n    Cbrt,\n    Pow,\n    Exp,\n    Exp2,\n    Expm1,\n    // Logarithms\n    Log,\n    Log1p,\n    Log2,\n    Log10,\n    // Float properties\n    Fabs,\n    Isnan,\n    Isinf,\n    Isfinite,\n    Copysign,\n    Isclose,\n    Nextafter,\n    Ulp,\n    // Trigonometric\n    Sin,\n    Cos,\n    Tan,\n    Asin,\n    Acos,\n    Atan,\n    Atan2,\n    // Hyperbolic\n    Sinh,\n    Cosh,\n    Tanh,\n    Asinh,\n    Acosh,\n    Atanh,\n    // Angular conversion\n    Degrees,\n    Radians,\n    // Integer math\n    Factorial,\n    Gcd,\n    Lcm,\n    Comb,\n    Perm,\n    // Modular / decomposition\n    Fmod,\n    Remainder,\n    Modf,\n    Frexp,\n    Ldexp,\n    // Special functions\n    Gamma,\n    Lgamma,\n    Erf,\n    Erfc,\n}\n\n/// Creates the `math` module and allocates it on the heap.\n///\n/// Registers all math functions and constants (`pi`, `e`, `tau`, `inf`, `nan`)\n/// matching CPython's `math` module. Functions are registered as\n/// `ModuleFunctions::Math` variants.\n///\n/// # Returns\n/// A `HeapId` pointing to the newly allocated module.\n///\n/// # Panics\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Math);\n\n    // Register all math functions\n    for (name, func) in MATH_FUNCTIONS {\n        module.set_attr(*name, Value::ModuleFunction(ModuleFunctions::Math(*func)), vm);\n    }\n\n    // Constants\n    module.set_attr(StaticStrings::Pi, Value::Float(std::f64::consts::PI), vm);\n    module.set_attr(StaticStrings::MathE, Value::Float(std::f64::consts::E), vm);\n    module.set_attr(StaticStrings::Tau, Value::Float(std::f64::consts::TAU), vm);\n    module.set_attr(StaticStrings::MathInf, Value::Float(f64::INFINITY), vm);\n    module.set_attr(StaticStrings::MathNan, Value::Float(f64::NAN), vm);\n\n    vm.heap.allocate(HeapData::Module(module))\n}\n\n/// Static mapping of attribute names to math functions for module creation.\nconst MATH_FUNCTIONS: &[(StaticStrings, MathFunctions)] = &[\n    // Rounding\n    (StaticStrings::Floor, MathFunctions::Floor),\n    (StaticStrings::Ceil, MathFunctions::Ceil),\n    (StaticStrings::Trunc, MathFunctions::Trunc),\n    // Roots & powers\n    (StaticStrings::Sqrt, MathFunctions::Sqrt),\n    (StaticStrings::Isqrt, MathFunctions::Isqrt),\n    (StaticStrings::Cbrt, MathFunctions::Cbrt),\n    (StaticStrings::Pow, MathFunctions::Pow),\n    (StaticStrings::Exp, MathFunctions::Exp),\n    (StaticStrings::Exp2, MathFunctions::Exp2),\n    (StaticStrings::Expm1, MathFunctions::Expm1),\n    // Logarithms\n    (StaticStrings::Log, MathFunctions::Log),\n    (StaticStrings::Log1p, MathFunctions::Log1p),\n    (StaticStrings::Log2, MathFunctions::Log2),\n    (StaticStrings::Log10, MathFunctions::Log10),\n    // Float properties\n    (StaticStrings::Fabs, MathFunctions::Fabs),\n    (StaticStrings::Isnan, MathFunctions::Isnan),\n    (StaticStrings::Isinf, MathFunctions::Isinf),\n    (StaticStrings::Isfinite, MathFunctions::Isfinite),\n    (StaticStrings::Copysign, MathFunctions::Copysign),\n    (StaticStrings::Isclose, MathFunctions::Isclose),\n    (StaticStrings::Nextafter, MathFunctions::Nextafter),\n    (StaticStrings::Ulp, MathFunctions::Ulp),\n    // Trigonometric\n    (StaticStrings::Sin, MathFunctions::Sin),\n    (StaticStrings::Cos, MathFunctions::Cos),\n    (StaticStrings::Tan, MathFunctions::Tan),\n    (StaticStrings::Asin, MathFunctions::Asin),\n    (StaticStrings::Acos, MathFunctions::Acos),\n    (StaticStrings::Atan, MathFunctions::Atan),\n    (StaticStrings::Atan2, MathFunctions::Atan2),\n    // Hyperbolic\n    (StaticStrings::Sinh, MathFunctions::Sinh),\n    (StaticStrings::Cosh, MathFunctions::Cosh),\n    (StaticStrings::Tanh, MathFunctions::Tanh),\n    (StaticStrings::Asinh, MathFunctions::Asinh),\n    (StaticStrings::Acosh, MathFunctions::Acosh),\n    (StaticStrings::Atanh, MathFunctions::Atanh),\n    // Angular conversion\n    (StaticStrings::Degrees, MathFunctions::Degrees),\n    (StaticStrings::Radians, MathFunctions::Radians),\n    // Integer math\n    (StaticStrings::Factorial, MathFunctions::Factorial),\n    (StaticStrings::Gcd, MathFunctions::Gcd),\n    (StaticStrings::Lcm, MathFunctions::Lcm),\n    (StaticStrings::Comb, MathFunctions::Comb),\n    (StaticStrings::Perm, MathFunctions::Perm),\n    // Modular / decomposition\n    (StaticStrings::Fmod, MathFunctions::Fmod),\n    (StaticStrings::Remainder, MathFunctions::Remainder),\n    (StaticStrings::Modf, MathFunctions::Modf),\n    (StaticStrings::Frexp, MathFunctions::Frexp),\n    (StaticStrings::Ldexp, MathFunctions::Ldexp),\n    // Special functions\n    (StaticStrings::Gamma, MathFunctions::Gamma),\n    (StaticStrings::Lgamma, MathFunctions::Lgamma),\n    (StaticStrings::Erf, MathFunctions::Erf),\n    (StaticStrings::Erfc, MathFunctions::Erfc),\n];\n\n/// Dispatches a call to a math module function.\n///\n/// All math functions are pure computations and return `Value` directly.\npub(super) fn call(\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n    function: MathFunctions,\n    args: ArgValues,\n) -> RunResult<Value> {\n    match function {\n        // Rounding\n        MathFunctions::Floor => math_floor(vm.heap, args),\n        MathFunctions::Ceil => math_ceil(vm.heap, args),\n        MathFunctions::Trunc => math_trunc(vm.heap, args),\n        // Roots & powers\n        MathFunctions::Sqrt => math_sqrt(vm.heap, args),\n        MathFunctions::Isqrt => math_isqrt(vm.heap, args),\n        MathFunctions::Cbrt => math_cbrt(vm.heap, args),\n        MathFunctions::Pow => math_pow(vm.heap, args),\n        MathFunctions::Exp => math_exp(vm.heap, args),\n        MathFunctions::Exp2 => math_exp2(vm.heap, args),\n        MathFunctions::Expm1 => math_expm1(vm.heap, args),\n        // Logarithms\n        MathFunctions::Log => math_log(vm.heap, args),\n        MathFunctions::Log1p => math_log1p(vm.heap, args),\n        MathFunctions::Log2 => math_log2(vm.heap, args),\n        MathFunctions::Log10 => math_log10(vm.heap, args),\n        // Float properties\n        MathFunctions::Fabs => math_fabs(vm.heap, args),\n        MathFunctions::Isnan => math_isnan(vm.heap, args),\n        MathFunctions::Isinf => math_isinf(vm.heap, args),\n        MathFunctions::Isfinite => math_isfinite(vm.heap, args),\n        MathFunctions::Copysign => math_copysign(vm.heap, args),\n        MathFunctions::Isclose => math_isclose(vm.heap, args, vm.interns),\n        MathFunctions::Nextafter => math_nextafter(vm.heap, args),\n        MathFunctions::Ulp => math_ulp(vm.heap, args),\n        // Trigonometric\n        MathFunctions::Sin => math_sin(vm.heap, args),\n        MathFunctions::Cos => math_cos(vm.heap, args),\n        MathFunctions::Tan => math_tan(vm.heap, args),\n        MathFunctions::Asin => math_asin(vm.heap, args),\n        MathFunctions::Acos => math_acos(vm.heap, args),\n        MathFunctions::Atan => math_atan(vm.heap, args),\n        MathFunctions::Atan2 => math_atan2(vm.heap, args),\n        // Hyperbolic\n        MathFunctions::Sinh => math_sinh(vm.heap, args),\n        MathFunctions::Cosh => math_cosh(vm.heap, args),\n        MathFunctions::Tanh => math_tanh(vm.heap, args),\n        MathFunctions::Asinh => math_asinh(vm.heap, args),\n        MathFunctions::Acosh => math_acosh(vm.heap, args),\n        MathFunctions::Atanh => math_atanh(vm.heap, args),\n        // Angular conversion\n        MathFunctions::Degrees => math_degrees(vm.heap, args),\n        MathFunctions::Radians => math_radians(vm.heap, args),\n        // Integer math\n        MathFunctions::Factorial => math_factorial(vm.heap, args),\n        MathFunctions::Gcd => math_gcd(vm.heap, args),\n        MathFunctions::Lcm => math_lcm(vm.heap, args),\n        MathFunctions::Comb => math_comb(vm.heap, args),\n        MathFunctions::Perm => math_perm(vm.heap, args),\n        // Modular / decomposition\n        MathFunctions::Fmod => math_fmod(vm.heap, args),\n        MathFunctions::Remainder => math_remainder(vm.heap, args),\n        MathFunctions::Modf => math_modf(vm.heap, args),\n        MathFunctions::Frexp => math_frexp(vm.heap, args),\n        MathFunctions::Ldexp => math_ldexp(vm.heap, args),\n        // Special functions\n        MathFunctions::Gamma => math_gamma(vm.heap, args),\n        MathFunctions::Lgamma => math_lgamma(vm.heap, args),\n        MathFunctions::Erf => math_erf(vm.heap, args),\n        MathFunctions::Erfc => math_erfc(vm.heap, args),\n    }\n}\n\n// ==========================\n// Rounding functions\n// ==========================\n\n/// `math.floor(x)` — returns the largest integer less than or equal to x.\n///\n/// Accepts int, float, or bool. Returns int.\n/// Raises `OverflowError` for infinity, `ValueError` for NaN.\nfn math_floor(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.floor\", heap)?;\n    defer_drop!(value, heap);\n\n    match value {\n        Value::Float(f) => float_to_int_checked(f.floor(), *f, heap),\n        Value::Int(n) => Ok(Value::Int(*n)),\n        Value::Bool(b) => Ok(Value::Int(i64::from(*b))),\n        _ => Err(ExcType::type_error(format!(\n            \"must be real number, not {}\",\n            value.py_type(heap)\n        ))),\n    }\n}\n\n/// `math.ceil(x)` — returns the smallest integer greater than or equal to x.\n///\n/// Accepts int, float, or bool. Returns int.\n/// Raises `OverflowError` for infinity, `ValueError` for NaN.\nfn math_ceil(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.ceil\", heap)?;\n    defer_drop!(value, heap);\n\n    match value {\n        Value::Float(f) => float_to_int_checked(f.ceil(), *f, heap),\n        Value::Int(n) => Ok(Value::Int(*n)),\n        Value::Bool(b) => Ok(Value::Int(i64::from(*b))),\n        _ => Err(ExcType::type_error(format!(\n            \"must be real number, not {}\",\n            value.py_type(heap)\n        ))),\n    }\n}\n\n/// `math.trunc(x)` — truncates x to the nearest integer toward zero.\n///\n/// Accepts int, float, or bool. Returns int.\nfn math_trunc(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.trunc\", heap)?;\n    defer_drop!(value, heap);\n\n    match value {\n        Value::Float(f) => float_to_int_checked(f.trunc(), *f, heap),\n        Value::Int(n) => Ok(Value::Int(*n)),\n        Value::Bool(b) => Ok(Value::Int(i64::from(*b))),\n        _ => Err(ExcType::type_error(format!(\n            \"type {} doesn't define __trunc__ method\",\n            value.py_type(heap)\n        ))),\n    }\n}\n\n// ==========================\n// Roots & powers\n// ==========================\n\n/// `math.sqrt(x)` — returns the square root of x.\n///\n/// Always returns a float. Raises `ValueError` for negative inputs with a\n/// descriptive message matching CPython 3.14: \"expected a nonnegative input, got <x>\".\nfn math_sqrt(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.sqrt\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f < 0.0 {\n        Err(SimpleException::new_msg(ExcType::ValueError, format!(\"expected a nonnegative input, got {f:?}\")).into())\n    } else {\n        Ok(Value::Float(f.sqrt()))\n    }\n}\n\n/// `math.isqrt(n)` — returns the integer square root of a non-negative integer.\n///\n/// Returns the largest integer `r` such that `r * r <= n`.\n/// Only accepts non-negative integers (and bools).\nfn math_isqrt(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.isqrt\", heap)?;\n    defer_drop!(value, heap);\n\n    let n = value_to_int(value, heap)?;\n    if n < 0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"isqrt() argument must be nonnegative\").into());\n    }\n    if n == 0 {\n        return Ok(Value::Int(0));\n    }\n\n    // Integer square root via f64 estimate + correction.\n    // For i64 inputs, f64 sqrt is accurate to within ±1, so we need to\n    // correct both overshoot and undershoot. The cast truncates toward zero,\n    // so undershoot is possible for perfect squares near f64 precision limits.\n    #[expect(\n        clippy::cast_precision_loss,\n        clippy::cast_possible_truncation,\n        reason = \"initial estimate doesn't need to be exact, correction refines it\"\n    )]\n    let mut x = (n as f64).sqrt() as i64;\n    // Correct overshoot: use `x > n / x` instead of `x * x > n` to avoid i64 overflow.\n    while x > n / x {\n        x -= 1;\n    }\n    // Correct undershoot: check if (x+1)² ≤ n using division to avoid overflow.\n    while x < n / (x + 1) {\n        x += 1;\n    }\n    Ok(Value::Int(x))\n}\n\n/// `math.cbrt(x)` — returns the cube root of x.\n///\n/// Always returns a float. Unlike `sqrt`, works for negative inputs.\nfn math_cbrt(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.cbrt\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.cbrt()))\n}\n\n/// `math.pow(x, y)` — returns x raised to the power y.\n///\n/// Always returns a float. Unlike the builtin `pow()`, does not support\n/// three-argument modular exponentiation. Raises `ValueError` for\n/// negative base with non-integer exponent.\nfn math_pow(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, y_val) = args.get_two_args(\"math.pow\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(y_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    let y = value_to_float(y_val, heap)?;\n    let result = x.powf(y);\n    // CPython raises ValueError for domain errors: 0**negative, negative**non-integer\n    if result.is_nan() && !x.is_nan() && !y.is_nan() {\n        return Err(math_domain_error());\n    }\n    if result.is_infinite() && x.is_finite() && y.is_finite() {\n        // 0**negative is a domain error (ValueError), not overflow\n        if x == 0.0 && y < 0.0 {\n            return Err(math_domain_error());\n        }\n        return Err(math_range_error());\n    }\n    Ok(Value::Float(result))\n}\n\n/// `math.exp(x)` — returns e raised to the power x.\nfn math_exp(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.exp\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let result = f.exp();\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n/// `math.exp2(x)` — returns 2 raised to the power x.\nfn math_exp2(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.exp2\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let result = f.exp2();\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n/// `math.expm1(x)` — returns e**x - 1.\n///\n/// More accurate than `exp(x) - 1` for small values of x.\nfn math_expm1(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.expm1\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let result = f.exp_m1();\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n// ==========================\n// Logarithms\n// ==========================\n\n/// `math.log(x[, base])` — returns the logarithm of x.\n///\n/// With one argument, returns the natural logarithm (base e).\n/// With two arguments, returns `log(x) / log(base)`.\n/// Raises `ValueError` for non-positive inputs (CPython 3.14: \"expected a positive input\").\nfn math_log(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, base_val) = args.get_one_two_args(\"math.log\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(base_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    if x <= 0.0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"expected a positive input\").into());\n    }\n\n    match base_val {\n        Some(base_v) => {\n            let base = value_to_float(base_v, heap)?;\n            // base == 1.0 causes division by zero in log(x)/log(base), matching\n            // CPython which raises ZeroDivisionError for this case.\n            #[expect(\n                clippy::float_cmp,\n                reason = \"exact comparison with 1.0 is intentional — log(1.0) is exactly 0.0\"\n            )]\n            if base == 1.0 {\n                return Err(SimpleException::new_msg(ExcType::ZeroDivisionError, \"division by zero\").into());\n            }\n            if base <= 0.0 {\n                return Err(SimpleException::new_msg(ExcType::ValueError, \"expected a positive input\").into());\n            }\n            Ok(Value::Float(x.ln() / base.ln()))\n        }\n        None => Ok(Value::Float(x.ln())),\n    }\n}\n\n/// `math.log1p(x)` — returns the natural logarithm of 1 + x.\n///\n/// More accurate than `log(1 + x)` for small values of x.\n/// CPython 3.14 raises ValueError with \"expected argument value > -1, got <x>\".\nfn math_log1p(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.log1p\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f <= -1.0 {\n        return Err(\n            SimpleException::new_msg(ExcType::ValueError, format!(\"expected argument value > -1, got {f:?}\")).into(),\n        );\n    }\n    Ok(Value::Float(f.ln_1p()))\n}\n\n/// `math.log2(x)` — returns the base-2 logarithm of x.\n///\n/// Returns `inf` for positive infinity, `nan` for NaN.\n/// Raises `ValueError` for non-positive finite inputs.\nfn math_log2(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.log2\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f <= 0.0 {\n        Err(SimpleException::new_msg(ExcType::ValueError, \"expected a positive input\").into())\n    } else {\n        Ok(Value::Float(f.log2()))\n    }\n}\n\n/// `math.log10(x)` — returns the base-10 logarithm of x.\n///\n/// Returns `inf` for positive infinity, `nan` for NaN.\n/// Raises `ValueError` for non-positive finite inputs.\nfn math_log10(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.log10\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f <= 0.0 {\n        Err(SimpleException::new_msg(ExcType::ValueError, \"expected a positive input\").into())\n    } else {\n        Ok(Value::Float(f.log10()))\n    }\n}\n\n// ==========================\n// Float properties\n// ==========================\n\n/// `math.fabs(x)` — returns the absolute value as a float.\n///\n/// Unlike the builtin `abs()`, always returns a float.\nfn math_fabs(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.fabs\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.abs()))\n}\n\n/// `math.isnan(x)` — returns True if x is NaN.\nfn math_isnan(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.isnan\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Bool(f.is_nan()))\n}\n\n/// `math.isinf(x)` — returns True if x is positive or negative infinity.\nfn math_isinf(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.isinf\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Bool(f.is_infinite()))\n}\n\n/// `math.isfinite(x)` — returns True if x is neither infinity nor NaN.\nfn math_isfinite(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.isfinite\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Bool(f.is_finite()))\n}\n\n/// `math.copysign(x, y)` — returns x with the sign of y.\n///\n/// Always returns a float.\nfn math_copysign(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, y_val) = args.get_two_args(\"math.copysign\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(y_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    let y = value_to_float(y_val, heap)?;\n    Ok(Value::Float(x.copysign(y)))\n}\n\n/// `math.isclose(a, b, *, rel_tol=1e-9, abs_tol=0.0)` — returns True if a and b are close.\n///\n/// Supports keyword-only `rel_tol` and `abs_tol` parameters matching CPython.\n/// Raises `ValueError` if either tolerance is negative.\nfn math_isclose(heap: &mut Heap<impl ResourceTracker>, args: ArgValues, interns: &Interns) -> RunResult<Value> {\n    let (positional, kwargs) = args.into_parts();\n    defer_drop_mut!(positional, heap);\n\n    // Extract exactly two positional args\n    let Some(a_val) = positional.next() else {\n        return Err(ExcType::type_error_at_least(\"math.isclose\", 2, 0));\n    };\n    defer_drop!(a_val, heap);\n    let Some(b_val) = positional.next() else {\n        return Err(ExcType::type_error_at_least(\"math.isclose\", 2, 1));\n    };\n    defer_drop!(b_val, heap);\n    if positional.len() > 0 {\n        return Err(ExcType::type_error_at_most(\"math.isclose\", 2, 2 + positional.len()));\n    }\n\n    let a = value_to_float(a_val, heap)?;\n    let b = value_to_float(b_val, heap)?;\n\n    // Parse optional keyword arguments rel_tol and abs_tol\n    let (rel_tol, abs_tol) = extract_isclose_kwargs(kwargs, heap, interns)?;\n\n    if rel_tol < 0.0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"tolerances must be non-negative\").into());\n    }\n    if abs_tol < 0.0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"tolerances must be non-negative\").into());\n    }\n\n    // Exact equality check matches CPython's isclose() behavior — two identical\n    // values (including infinities) are always considered close.\n    #[expect(\n        clippy::float_cmp,\n        reason = \"exact equality check matches CPython's isclose() semantics\"\n    )]\n    if a == b {\n        return Ok(Value::Bool(true));\n    }\n    if a.is_infinite() || b.is_infinite() {\n        return Ok(Value::Bool(false));\n    }\n    if a.is_nan() || b.is_nan() {\n        return Ok(Value::Bool(false));\n    }\n\n    let diff = (a - b).abs();\n    let result = diff <= (rel_tol * a.abs().max(b.abs())).max(abs_tol);\n    Ok(Value::Bool(result))\n}\n\n/// Extracts `rel_tol` and `abs_tol` keyword arguments for `math.isclose`.\n///\n/// Returns `(rel_tol, abs_tol)` with defaults of `(1e-9, 0.0)`.\nfn extract_isclose_kwargs(\n    kwargs: crate::args::KwargsValues,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<(f64, f64)> {\n    let mut rel_tol: f64 = 1e-9;\n    let mut abs_tol: f64 = 0.0;\n\n    if kwargs.is_empty() {\n        return Ok((rel_tol, abs_tol));\n    }\n\n    for (key, value) in kwargs {\n        defer_drop!(key, heap);\n        defer_drop!(value, heap);\n\n        let Some(keyword_name) = key.as_either_str(heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(interns);\n        match key_str {\n            \"rel_tol\" => {\n                rel_tol = value_to_float(value, heap)?;\n            }\n            \"abs_tol\" => {\n                abs_tol = value_to_float(value, heap)?;\n            }\n            other => {\n                return Err(ExcType::type_error(format!(\n                    \"isclose() got an unexpected keyword argument '{other}'\"\n                )));\n            }\n        }\n    }\n\n    Ok((rel_tol, abs_tol))\n}\n\n/// `math.nextafter(x, y)` — returns the next float after x towards y.\nfn math_nextafter(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, y_val) = args.get_two_args(\"math.nextafter\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(y_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    let y = value_to_float(y_val, heap)?;\n\n    Ok(Value::Float(libm::nextafter(x, y)))\n}\n\n/// `math.ulp(x)` — returns the value of the least significant bit of x.\n///\n/// For finite non-zero x, returns the smallest float `u` such that `x + u != x`.\n/// Special cases: `ulp(nan)` returns nan, `ulp(inf)` returns inf.\nfn math_ulp(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.ulp\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f.is_nan() {\n        return Ok(Value::Float(f64::NAN));\n    }\n    if f.is_infinite() {\n        return Ok(Value::Float(f64::INFINITY));\n    }\n    let f = f.abs();\n    if f == 0.0 {\n        // CPython returns the smallest positive subnormal: 5e-324\n        return Ok(Value::Float(f64::from_bits(1)));\n    }\n    // ULP = nextafter(f, inf) - f\n    let next = libm::nextafter(f, f64::INFINITY);\n    Ok(Value::Float(next - f))\n}\n\n// ==========================\n// Trigonometric functions\n// ==========================\n\n/// `math.sin(x)` — returns the sine of x (in radians).\n///\n/// CPython 3.14 raises ValueError for infinity: \"expected a finite input, got inf\".\nfn math_sin(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.sin\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    require_finite(f)?;\n    Ok(Value::Float(f.sin()))\n}\n\n/// `math.cos(x)` — returns the cosine of x (in radians).\nfn math_cos(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.cos\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    require_finite(f)?;\n    Ok(Value::Float(f.cos()))\n}\n\n/// `math.tan(x)` — returns the tangent of x (in radians).\nfn math_tan(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.tan\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    require_finite(f)?;\n    Ok(Value::Float(f.tan()))\n}\n\n/// `math.asin(x)` — returns the arc sine of x (in radians).\n///\n/// CPython 3.14: \"expected a number in range from -1 up to 1, got <x>\".\nfn math_asin(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.asin\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    require_unit_range(f)?;\n    Ok(Value::Float(f.asin()))\n}\n\n/// `math.acos(x)` — returns the arc cosine of x (in radians).\n///\n/// CPython 3.14: \"expected a number in range from -1 up to 1, got <x>\".\nfn math_acos(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.acos\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    require_unit_range(f)?;\n    Ok(Value::Float(f.acos()))\n}\n\n/// `math.atan(x)` — returns the arc tangent of x (in radians).\nfn math_atan(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.atan\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.atan()))\n}\n\n/// `math.atan2(y, x)` — returns atan(y/x) in radians, using the signs of both\n/// to determine the correct quadrant.\nfn math_atan2(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (y_val, x_val) = args.get_two_args(\"math.atan2\", heap)?;\n    defer_drop!(y_val, heap);\n    defer_drop!(x_val, heap);\n\n    let y = value_to_float(y_val, heap)?;\n    let x = value_to_float(x_val, heap)?;\n    Ok(Value::Float(y.atan2(x)))\n}\n\n// ==========================\n// Hyperbolic functions\n// ==========================\n\n/// `math.sinh(x)` — returns the hyperbolic sine of x.\nfn math_sinh(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.sinh\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let result = f.sinh();\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n/// `math.cosh(x)` — returns the hyperbolic cosine of x.\nfn math_cosh(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.cosh\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let result = f.cosh();\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n/// `math.tanh(x)` — returns the hyperbolic tangent of x.\nfn math_tanh(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.tanh\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.tanh()))\n}\n\n/// `math.asinh(x)` — returns the inverse hyperbolic sine of x.\nfn math_asinh(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.asinh\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.asinh()))\n}\n\n/// `math.acosh(x)` — returns the inverse hyperbolic cosine of x.\n///\n/// CPython 3.14: \"expected argument value not less than 1, got <x>\".\nfn math_acosh(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.acosh\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f < 1.0 {\n        return Err(SimpleException::new_msg(\n            ExcType::ValueError,\n            format!(\"expected argument value not less than 1, got {f:?}\"),\n        )\n        .into());\n    }\n    Ok(Value::Float(f.acosh()))\n}\n\n/// `math.atanh(x)` — returns the inverse hyperbolic tangent of x.\n///\n/// CPython 3.14: \"expected a number between -1 and 1, got <x>\".\nfn math_atanh(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.atanh\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    if f <= -1.0 || f >= 1.0 {\n        return Err(SimpleException::new_msg(\n            ExcType::ValueError,\n            format!(\"expected a number between -1 and 1, got {f:?}\"),\n        )\n        .into());\n    }\n    Ok(Value::Float(f.atanh()))\n}\n\n// ==========================\n// Angular conversion\n// ==========================\n\n/// `math.degrees(x)` — converts angle x from radians to degrees.\nfn math_degrees(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.degrees\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.to_degrees()))\n}\n\n/// `math.radians(x)` — converts angle x from degrees to radians.\nfn math_radians(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.radians\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(f.to_radians()))\n}\n\n// ==========================\n// Integer math\n// ==========================\n\n/// `math.factorial(n)` — returns n factorial.\n///\n/// Only accepts non-negative integers (and bools). Raises `ValueError` for\n/// negative values, `TypeError` for non-integer types.\nfn math_factorial(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.factorial\", heap)?;\n    defer_drop!(value, heap);\n\n    let n = match value {\n        Value::Int(n) => *n,\n        Value::Bool(b) => i64::from(*b),\n        _ => {\n            return Err(ExcType::type_error(format!(\n                \"'{}' object cannot be interpreted as an integer\",\n                value.py_type(heap)\n            )));\n        }\n    };\n\n    if n < 0 {\n        return Err(\n            SimpleException::new_msg(ExcType::ValueError, \"factorial() not defined for negative values\").into(),\n        );\n    }\n\n    // Compute factorial iteratively\n    let mut result: i64 = 1;\n    for i in 2..=n {\n        match result.checked_mul(i) {\n            Some(v) => result = v,\n            None => {\n                // Overflow — for simplicity, return an error for very large factorials\n                // since we don't have LongInt factorial support yet\n                return Err(\n                    SimpleException::new_msg(ExcType::OverflowError, \"int too large to convert to factorial\").into(),\n                );\n            }\n        }\n    }\n    Ok(Value::Int(result))\n}\n\n/// `math.gcd(*integers)` — returns the greatest common divisor of the arguments.\n///\n/// Supports 0 or more arguments, matching CPython 3.9+. `gcd()` returns 0,\n/// `gcd(n)` returns `abs(n)`, and for multiple args reduces pairwise.\n/// The result is always non-negative.\nfn math_gcd(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let positional = args.into_pos_only(\"math.gcd\", heap)?;\n    defer_drop_mut!(positional, heap);\n\n    let mut result: u64 = 0;\n    for arg in positional.by_ref() {\n        defer_drop!(arg, heap);\n        let n = value_to_int(arg, heap)?;\n        result = gcd(result, n.unsigned_abs());\n    }\n    u64_to_value(result, heap)\n}\n\n/// `math.lcm(*integers)` — returns the least common multiple of the arguments.\n///\n/// Supports 0 or more arguments, matching CPython 3.9+. `lcm()` returns 1,\n/// `lcm(n)` returns `abs(n)`, and for multiple args reduces pairwise.\n/// The result is always non-negative. Returns 0 if any argument is 0.\nfn math_lcm(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let positional = args.into_pos_only(\"math.lcm\", heap)?;\n    defer_drop_mut!(positional, heap);\n\n    let mut result: u64 = 1;\n    for arg in positional.by_ref() {\n        defer_drop!(arg, heap);\n        let n = value_to_int(arg, heap)?;\n        let abs_n = n.unsigned_abs();\n        if abs_n == 0 {\n            return Ok(Value::Int(0));\n        }\n        let g = gcd(result, abs_n);\n        // lcm(a, b) = |a| / gcd(a,b) * |b| — dividing first avoids intermediate overflow\n        result = (result / g)\n            .checked_mul(abs_n)\n            .ok_or_else(|| SimpleException::new_msg(ExcType::OverflowError, \"integer overflow in lcm\"))?;\n    }\n    u64_to_value(result, heap)\n}\n\n/// `math.comb(n, k)` — returns the number of ways to choose k items from n.\n///\n/// Both arguments must be non-negative integers.\nfn math_comb(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (n_val, k_val) = args.get_two_args(\"math.comb\", heap)?;\n    defer_drop!(n_val, heap);\n    defer_drop!(k_val, heap);\n\n    let n = value_to_int(n_val, heap)?;\n    let k = value_to_int(k_val, heap)?;\n\n    if n < 0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"n must be a non-negative integer\").into());\n    }\n    if k < 0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"k must be a non-negative integer\").into());\n    }\n    if k > n {\n        return Ok(Value::Int(0));\n    }\n\n    // Use the smaller of k and n-k for efficiency: C(n, k) = C(n, n-k)\n    let k = k.min(n - k);\n    let mut result: i64 = 1;\n    for i in 0..k {\n        // Use GCD reduction to keep intermediates small:\n        // result = result * (n - i) / (i + 1)\n        // By dividing both numerator and denominator by their GCD first,\n        // we reduce the chance of overflow in the multiplication step.\n        let mut numerator = n - i;\n        let mut denominator = i + 1;\n        #[expect(clippy::cast_sign_loss, reason = \"both values are known non-negative at this point\")]\n        let g = gcd(numerator as u64, denominator as u64).cast_signed();\n        numerator /= g;\n        denominator /= g;\n        // Also reduce against the running result\n        #[expect(clippy::cast_sign_loss, reason = \"result and denominator are known non-negative\")]\n        let g2 = gcd(result as u64, denominator as u64).cast_signed();\n        result /= g2;\n        denominator /= g2;\n        debug_assert!(denominator == 1, \"denominator should be 1 after GCD reduction in comb\");\n        match result.checked_mul(numerator) {\n            Some(v) => result = v,\n            None => {\n                return Err(SimpleException::new_msg(ExcType::OverflowError, \"integer overflow in comb\").into());\n            }\n        }\n    }\n    Ok(Value::Int(result))\n}\n\n/// `math.perm(n, k=None)` — returns the number of k-length permutations from n items.\n///\n/// Both arguments must be non-negative integers. When `k` is omitted, defaults to `n`\n/// (i.e., `perm(n)` returns `n!`), matching CPython behavior.\nfn math_perm(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (n_val, k_val) = args.get_one_two_args(\"math.perm\", heap)?;\n    defer_drop!(n_val, heap);\n\n    let n = value_to_int(n_val, heap)?;\n    let k_explicit = k_val.is_some();\n    let k = match k_val {\n        Some(kv) => {\n            defer_drop!(kv, heap);\n            value_to_int(kv, heap)?\n        }\n        None => n,\n    };\n\n    if n < 0 {\n        // When called as perm(n) without k, CPython uses the factorial error message\n        let msg = if k_explicit {\n            \"n must be a non-negative integer\"\n        } else {\n            \"factorial() not defined for negative values\"\n        };\n        return Err(SimpleException::new_msg(ExcType::ValueError, msg).into());\n    }\n    if k < 0 {\n        return Err(SimpleException::new_msg(ExcType::ValueError, \"k must be a non-negative integer\").into());\n    }\n    if k > n {\n        return Ok(Value::Int(0));\n    }\n\n    let mut result: i64 = 1;\n    for i in 0..k {\n        match result.checked_mul(n - i) {\n            Some(v) => result = v,\n            None => {\n                return Err(SimpleException::new_msg(ExcType::OverflowError, \"integer overflow in perm\").into());\n            }\n        }\n    }\n    Ok(Value::Int(result))\n}\n\n// ==========================\n// Modular / decomposition\n// ==========================\n\n/// `math.fmod(x, y)` — returns x modulo y as a float.\n///\n/// Unlike `x % y`, the result has the same sign as x. Raises `ValueError`\n/// when y is zero (CPython: \"math domain error\").\nfn math_fmod(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, y_val) = args.get_two_args(\"math.fmod\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(y_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    let y = value_to_float(y_val, heap)?;\n\n    if y == 0.0 || x.is_infinite() {\n        // CPython raises for both fmod(x, 0) and fmod(inf, y)\n        // but NaN inputs propagate\n        if !x.is_nan() && !y.is_nan() {\n            return Err(math_domain_error());\n        }\n    }\n    Ok(Value::Float(x % y))\n}\n\n/// `math.remainder(x, y)` — IEEE 754 remainder of x with respect to y.\n///\n/// The result is `x - n*y` where n is the closest integer to `x/y`.\nfn math_remainder(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, y_val) = args.get_two_args(\"math.remainder\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(y_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    let y = value_to_float(y_val, heap)?;\n\n    // NaN propagates\n    if x.is_nan() || y.is_nan() {\n        return Ok(Value::Float(f64::NAN));\n    }\n    if y == 0.0 {\n        return Err(math_domain_error());\n    }\n    if x.is_infinite() {\n        return Err(math_domain_error());\n    }\n    if y.is_infinite() {\n        return Ok(Value::Float(x));\n    }\n\n    Ok(Value::Float(libm::remainder(x, y)))\n}\n\n/// `math.modf(x)` — returns the fractional and integer parts of x as a tuple.\n///\n/// Both values carry the sign of x. Returns `(fractional, integer)`.\nfn math_modf(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.modf\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let (fractional, integer) = libm::modf(f);\n    let tuple = allocate_tuple(smallvec![Value::Float(fractional), Value::Float(integer)], heap)?;\n    Ok(tuple)\n}\n\n/// `math.frexp(x)` — returns (mantissa, exponent) such that `x == mantissa * 2**exponent`.\n///\n/// The mantissa is always in the range [0.5, 1.0) or zero.\n/// Returns a tuple `(float, int)`.\nfn math_frexp(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.frexp\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    let (m, exp) = libm::frexp(f);\n    let tuple = allocate_tuple(smallvec![Value::Float(m), Value::Int(i64::from(exp))], heap)?;\n    Ok(tuple)\n}\n\n/// `math.ldexp(x, i)` — returns `x * 2**i`, the inverse of `frexp`.\n///\n/// Clamps the exponent to `i32` range before calling `libm::ldexp`, which is safe\n/// because IEEE 754 double exponents only span -1074 to +1023 — any `i64` outside\n/// `i32` range would trivially overflow or underflow anyway.\nfn math_ldexp(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (x_val, i_val) = args.get_two_args(\"math.ldexp\", heap)?;\n    defer_drop!(x_val, heap);\n    defer_drop!(i_val, heap);\n\n    let x = value_to_float(x_val, heap)?;\n    let i = value_to_int(i_val, heap)?;\n\n    // Special cases: inf/nan/zero pass through regardless of exponent\n    if x.is_nan() || x.is_infinite() || x == 0.0 {\n        return Ok(Value::Float(x));\n    }\n\n    // Clamp i64 to i32 range — exponents beyond ±2 billion trivially overflow/underflow\n    #[expect(clippy::cast_possible_truncation, reason = \"clamped to i32 range first\")]\n    let exp = i.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;\n    let result = libm::ldexp(x, exp);\n\n    // If the result overflowed to infinity, CPython raises OverflowError\n    if result.is_infinite() {\n        return Err(math_range_error());\n    }\n\n    Ok(Value::Float(result))\n}\n\n// ==========================\n// Special functions\n// ==========================\n\n/// `math.gamma(x)` — returns the Gamma function at x.\n///\n/// CPython 3.14 raises ValueError for non-positive integers:\n/// \"expected a noninteger or positive integer, got <x>\".\nfn math_gamma(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.gamma\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    // CPython also rejects -inf for gamma (but not lgamma, where lgamma(-inf) = inf)\n    if f == f64::NEG_INFINITY {\n        return Err(SimpleException::new_msg(\n            ExcType::ValueError,\n            format!(\"expected a noninteger or positive integer, got {f:?}\"),\n        )\n        .into());\n    }\n    check_gamma_pole(f)?;\n\n    let result = libm::tgamma(f);\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n/// `math.lgamma(x)` — returns the natural log of the absolute value of Gamma(x).\nfn math_lgamma(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.lgamma\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    check_gamma_pole(f)?;\n\n    let result = libm::lgamma(f);\n    check_range_error(result, f)?;\n    Ok(Value::Float(result))\n}\n\n/// `math.erf(x)` — returns the error function at x.\nfn math_erf(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.erf\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(libm::erf(f)))\n}\n\n/// `math.erfc(x)` — returns the complementary error function at x (1 - erf(x)).\n///\n/// More accurate than `1 - erf(x)` for large x.\nfn math_erfc(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let value = args.get_one_arg(\"math.erfc\", heap)?;\n    defer_drop!(value, heap);\n\n    let f = value_to_float(value, heap)?;\n    Ok(Value::Float(libm::erfc(f)))\n}\n\n// ==========================\n// Helper functions\n// ==========================\n\n/// Converts a rounded float to an integer `Value`, checking for infinity/NaN.\n///\n/// `rounded` is the already-rounded float value (e.g., from `floor()`, `ceil()`, `trunc()`).\n/// `original` is the original input float, used only to determine the error type:\n/// infinity produces `OverflowError`, NaN produces `ValueError`.\n///\n/// For finite values outside the i64 range, promotes to `LongInt` to match CPython's\n/// behavior of returning arbitrary-precision integers from `math.floor`/`ceil`/`trunc`.\nfn float_to_int_checked(rounded: f64, original: f64, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if original.is_infinite() {\n        Err(SimpleException::new_msg(ExcType::OverflowError, \"cannot convert float infinity to integer\").into())\n    } else if original.is_nan() {\n        Err(SimpleException::new_msg(ExcType::ValueError, \"cannot convert float NaN to integer\").into())\n    } else if rounded >= i64::MIN as f64 && rounded < i64::MAX as f64 {\n        // Note: `i64::MAX as f64` rounds up to 2^63 (9223372036854775808.0), so we use\n        // strict less-than to exclude that value. `i64::MIN as f64` is exact (-2^63).\n        #[expect(\n            clippy::cast_possible_truncation,\n            reason = \"intentional: value is within i64 range after bounds check\"\n        )]\n        let result = rounded as i64;\n        Ok(Value::Int(result))\n    } else {\n        // Value exceeds i64 range — promote to LongInt.\n        // Format with no decimal places and parse as BigInt. This is correct because\n        // `rounded` is already an integer-valued float from floor/ceil/trunc.\n        let s = format!(\"{rounded:.0}\");\n        let bi = s\n            .parse::<BigInt>()\n            .map_err(|_| SimpleException::new_msg(ExcType::ValueError, \"float too large to convert to integer\"))?;\n        Ok(LongInt::new(bi).into_value(heap)?)\n    }\n}\n\n/// Converts a `Value` to `f64`, raising `TypeError` if the value is not numeric.\n///\n/// Accepts `Float`, `Int`, and `Bool` values. For other types, raises a `TypeError`\n/// with a message matching CPython's format: \"must be real number, not <type>\".\n#[expect(\n    clippy::cast_precision_loss,\n    reason = \"i64-to-f64 can lose precision for large integers (beyond 2^53), but this matches CPython's conversion semantics\"\n)]\nfn value_to_float(value: &Value, heap: &Heap<impl ResourceTracker>) -> RunResult<f64> {\n    match value {\n        Value::Float(f) => Ok(*f),\n        Value::Int(n) => Ok(*n as f64),\n        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),\n        _ => Err(ExcType::type_error(format!(\n            \"must be real number, not {}\",\n            value.py_type(heap)\n        ))),\n    }\n}\n\n/// Converts a `Value` to `i64`, raising `TypeError` if the value is not an integer.\n///\n/// Accepts `Int` and `Bool` values. For other types, raises a `TypeError`\n/// with a message matching CPython's format.\nfn value_to_int(value: &Value, heap: &Heap<impl ResourceTracker>) -> RunResult<i64> {\n    match value {\n        Value::Int(n) => Ok(*n),\n        Value::Bool(b) => Ok(i64::from(*b)),\n        _ => Err(ExcType::type_error(format!(\n            \"'{}' object cannot be interpreted as an integer\",\n            value.py_type(heap)\n        ))),\n    }\n}\n\n/// Requires that a float is finite, raising ValueError if it's inf or nan.\n///\n/// CPython 3.14 uses \"expected a finite input, got inf\" for trig functions.\nfn require_finite(f: f64) -> RunResult<()> {\n    if f.is_infinite() {\n        Err(SimpleException::new_msg(ExcType::ValueError, format!(\"expected a finite input, got {f:?}\")).into())\n    } else {\n        Ok(())\n    }\n}\n\n/// Euclidean GCD algorithm for unsigned 64-bit integers.\nfn gcd(mut a: u64, mut b: u64) -> u64 {\n    while b != 0 {\n        let t = b;\n        b = a % b;\n        a = t;\n    }\n    a\n}\n\n/// Converts a `u64` result to a `Value`, promoting to `LongInt` if it exceeds `i64::MAX`.\n///\n/// This is needed for operations like `gcd(i64::MIN, 0)` where the unsigned result\n/// (`2^63`) doesn't fit in a signed `i64`.\nfn u64_to_value(n: u64, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if let Ok(signed) = i64::try_from(n) {\n        Ok(Value::Int(signed))\n    } else {\n        Ok(LongInt::new(BigInt::from(n)).into_value(heap)?)\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/modules/mod.rs",
    "content": "//! Built-in module implementations.\n//!\n//! This module provides implementations for Python built-in modules like `sys`, `typing`,\n//! and `asyncio`. These are created on-demand when import statements are executed.\n\nuse std::fmt::{self, Write};\n\nuse strum::FromRepr;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    exception_private::RunResult,\n    heap::HeapId,\n    intern::{StaticStrings, StringId},\n    resource::{ResourceError, ResourceTracker},\n};\n\npub(crate) mod asyncio;\npub(crate) mod math;\npub(crate) mod os;\npub(crate) mod pathlib;\npub(crate) mod re;\npub(crate) mod sys;\npub(crate) mod typing;\n\n/// Built-in modules that can be imported.\n#[repr(u8)]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr)]\npub(crate) enum BuiltinModule {\n    /// The `sys` module providing system-specific parameters and functions.\n    Sys,\n    /// The `typing` module providing type hints support.\n    Typing,\n    /// The `asyncio` module providing async/await support (only `gather()` implemented).\n    Asyncio,\n    /// The `pathlib` module providing object-oriented filesystem paths.\n    Pathlib,\n    /// The `os` module providing operating system interface (only `getenv()` implemented).\n    Os,\n    /// The `math` module providing mathematical functions and constants.\n    Math,\n    /// The `re` module providing regular expression matching.\n    Re,\n}\n\nimpl BuiltinModule {\n    /// Get the module from a string ID.\n    pub fn from_string_id(string_id: StringId) -> Option<Self> {\n        match StaticStrings::from_string_id(string_id)? {\n            StaticStrings::Sys => Some(Self::Sys),\n            StaticStrings::Typing => Some(Self::Typing),\n            StaticStrings::Asyncio => Some(Self::Asyncio),\n            StaticStrings::Pathlib => Some(Self::Pathlib),\n            StaticStrings::Os => Some(Self::Os),\n            StaticStrings::Math => Some(Self::Math),\n            StaticStrings::Re => Some(Self::Re),\n            _ => None,\n        }\n    }\n\n    /// Creates a new instance of this module on the heap.\n    ///\n    /// Returns a HeapId pointing to the newly allocated module.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the required strings have not been pre-interned during prepare phase.\n    pub fn create(self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n        match self {\n            Self::Sys => sys::create_module(vm),\n            Self::Typing => typing::create_module(vm),\n            Self::Asyncio => asyncio::create_module(vm),\n            Self::Pathlib => pathlib::create_module(vm),\n            Self::Os => os::create_module(vm),\n            Self::Math => math::create_module(vm),\n            Self::Re => re::create_module(vm),\n        }\n    }\n}\n\n/// All stdlib module function (but not builtins).\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) enum ModuleFunctions {\n    Asyncio(asyncio::AsyncioFunctions),\n    Math(math::MathFunctions),\n    Os(os::OsFunctions),\n    Re(re::ReFunctions),\n}\n\nimpl fmt::Display for ModuleFunctions {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Asyncio(func) => write!(f, \"{func}\"),\n            Self::Math(func) => write!(f, \"{func}\"),\n            Self::Os(func) => write!(f, \"{func}\"),\n            Self::Re(func) => write!(f, \"{func}\"),\n        }\n    }\n}\n\nimpl ModuleFunctions {\n    /// Calls the module function with the given arguments.\n    ///\n    /// Returns `CallResult` to support both immediate values and OS calls that\n    /// require host involvement (e.g., `os.getenv()` needs the host to provide environment variables).\n    pub fn call(self, vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {\n        match self {\n            Self::Asyncio(functions) => asyncio::call(vm.heap, functions, args),\n            Self::Math(functions) => math::call(vm, functions, args).map(CallResult::Value),\n            Self::Os(functions) => os::call(vm.heap, functions, args),\n            Self::Re(functions) => re::call(vm, functions, args),\n        }\n    }\n\n    /// Writes the Python repr() string for this function to a formatter.\n    pub fn py_repr_fmt<W: Write>(self, f: &mut W, py_id: usize) -> std::fmt::Result {\n        write!(f, \"<function {self} at 0x{py_id:x}>\")\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/modules/os.rs",
    "content": "//! Implementation of the `os` module.\n//!\n//! Provides a minimal implementation of Python's `os` module with:\n//! - `getenv(key, default=None)`: Get a single environment variable\n//! - `environ`: Property that returns the entire environment as a dict\n//!\n//! Other os functions are not implemented. OS operations require host involvement\n//! via the `OsFunction` callback mechanism - Monty yields control to the host\n//! which executes the operation and returns the result.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapData, HeapId},\n    intern::StaticStrings,\n    modules::ModuleFunctions,\n    os::OsFunction,\n    resource::{ResourceError, ResourceTracker},\n    types::{Module, Property, PyTrait},\n    value::Value,\n};\n\n/// OS module functions.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, serde::Serialize, serde::Deserialize)]\n#[strum(serialize_all = \"lowercase\")]\npub(crate) enum OsFunctions {\n    Getenv,\n}\n\n/// Creates the `os` module and allocates it on the heap.\n///\n/// The module provides:\n/// - `getenv(key, default=None)`: Get a single environment variable\n/// - `environ`: Property that returns the entire environment as a dict\n///\n/// Both operations yield to the host via `OsFunction` callbacks.\n///\n/// # Returns\n/// A HeapId pointing to the newly allocated module.\n///\n/// # Panics\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Os);\n\n    // os.getenv - function to get a single environment variable\n    module.set_attr(\n        StaticStrings::Getenv,\n        Value::ModuleFunction(ModuleFunctions::Os(OsFunctions::Getenv)),\n        vm,\n    );\n\n    // os.environ - property that returns the entire environment as a dict\n    module.set_attr(\n        StaticStrings::Environ,\n        Value::Property(Property::Os(OsFunction::GetEnviron)),\n        vm,\n    );\n\n    vm.heap.allocate(HeapData::Module(module))\n}\n\n/// Dispatches a call to an os module function.\n///\n/// Returns `CallResult::OsCall` for functions that need host involvement,\n/// or `CallResult::Value` for functions that can be computed immediately.\npub(super) fn call(\n    heap: &mut Heap<impl ResourceTracker>,\n    functions: OsFunctions,\n    args: ArgValues,\n) -> RunResult<CallResult> {\n    match functions {\n        OsFunctions::Getenv => getenv(heap, args),\n    }\n}\n\n/// Implementation of `os.getenv(key, default=None)`.\n///\n/// Returns the value of the environment variable `key` if it exists, or `default` if it doesn't.\n/// This function yields to the host to perform the actual environment lookup.\n///\n/// # Arguments\n/// * `heap` - The heap for any allocations\n/// * `args` - Function arguments: `key` (required string), `default` (optional, defaults to None)\n///\n/// # Returns\n/// `CallResult::OsCall` with `OsFunction::Getenv` - the host should look up the\n/// environment variable and return the value, or the default if not found.\n///\n/// # Errors\n/// Returns `TypeError` if:\n/// - No arguments are provided\n/// - More than 2 arguments are provided\n/// - `key` is not a string\nfn getenv(heap: &mut Heap<impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {\n    // getenv(key, default=None) - accepts 1 or 2 positional arguments\n    let (key, default) = args.get_one_two_args(\"os.getenv\", heap)?;\n\n    // Validate key is a string\n    if key.is_str(heap) {\n        // Build args to pass to host: (key, default)\n        // The default is Value::None if not provided\n        let final_default = default.unwrap_or(Value::None);\n        let args = ArgValues::Two(key, final_default);\n\n        Ok(CallResult::OsCall(OsFunction::Getenv, args))\n    } else {\n        let type_name = key.py_type(heap);\n        key.drop_with_heap(heap);\n        if let Some(d) = default {\n            d.drop_with_heap(heap);\n        }\n        Err(ExcType::type_error(format!(\"str expected, not {type_name}\")))\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/modules/pathlib.rs",
    "content": "//! Implementation of the `pathlib` module.\n//!\n//! Provides a minimal implementation of Python's `pathlib` module with:\n//! - `Path`: A class for filesystem path operations\n//!\n//! The `Path` class supports both pure methods (no I/O, handled directly) and\n//! filesystem methods (require I/O, yield external function calls for host resolution).\n\nuse crate::{\n    builtins::Builtins,\n    bytecode::VM,\n    heap::{HeapData, HeapId},\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    types::{Module, Type},\n    value::Value,\n};\n\n/// Creates the `pathlib` module and allocates it on the heap.\n///\n/// Returns a HeapId pointing to the newly allocated module.\n///\n/// # Panics\n///\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Pathlib);\n\n    // pathlib.Path - the Path class (callable to create Path instances)\n    module.set_attr(StaticStrings::PathClass, Value::Builtin(Builtins::Type(Type::Path)), vm);\n\n    vm.heap.allocate(HeapData::Module(module))\n}\n"
  },
  {
    "path": "crates/monty/src/modules/re.rs",
    "content": "//! Implementation of the `re` module.\n//!\n//! Provides regular expression matching operations.\n//! Uses the Rust `fancy-regex` crate.\n//!\n//! # Supported module-level functions\n//!\n//! - `re.compile(pattern, flags=0)` → `re.Pattern`\n//! - `re.search(pattern, string, flags=0)` → `re.Match` or `None`\n//! - `re.match(pattern, string, flags=0)` → `re.Match` or `None`\n//! - `re.fullmatch(pattern, string, flags=0)` → `re.Match` or `None`\n//! - `re.findall(pattern, string, flags=0)` → `list`\n//! - `re.sub(pattern, repl, string, count=0, flags=0)` → `str`\n//! - `re.split(pattern, string, maxsplit=0, flags=0)` → `list`\n//! - `re.finditer(pattern, string, flags=0)` → iterator of `re.Match`\n//! - `re.escape(pattern)` → `str`\n//!\n//! # Module attributes\n//!\n//! - `re.NOFLAG` - no flag (value: 0)\n//! - `re.IGNORECASE` / `re.I` — case-insensitive matching (value: 2)\n//! - `re.MULTILINE` / `re.M` — `^`/`$` match at line boundaries (value: 8)\n//! - `re.DOTALL` / `re.S` — `.` matches newlines (value: 16)\n//! - `re.ASCII` / `re.A` — ASCII-only matching for `\\w`, `\\d`, `\\s` (value: 256)\n//! - `re.PatternError` / `re.error` — exception type for invalid patterns\n\nuse std::borrow::Cow;\n\nuse crate::{\n    args::ArgValues,\n    builtins::Builtins,\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult},\n    heap::{DropWithHeap, Heap, HeapData, HeapId},\n    intern::StaticStrings,\n    modules::ModuleFunctions,\n    resource::{ResourceError, ResourceTracker},\n    types::{Module, PyTrait, RePattern, Str, Type, re_pattern::value_to_str},\n    value::Value,\n};\n\n/// Python regex flag: no flag being applied.\npub(crate) const NOFLAG: u16 = 0;\n/// Python regex flag: case-insensitive matching.\npub(crate) const IGNORECASE: u16 = 2;\n/// Python regex flag: `^` and `$` match at line boundaries.\npub(crate) const MULTILINE: u16 = 8;\n/// Python regex flag: `.` matches newlines.\npub(crate) const DOTALL: u16 = 16;\n/// Python regex flag: ASCII-only matching for `\\w`, `\\b`, `\\d`, `\\s`.\npub(crate) const ASCII: u16 = 256;\n\n/// Functions exposed by the `re` module.\n///\n/// Each variant corresponds to a module-level function that can be called directly\n/// (e.g., `re.search(pattern, string)`). These are convenience wrappers that compile\n/// the pattern on each call — for repeated use, `re.compile()` avoids recompilation.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, serde::Serialize, serde::Deserialize)]\n#[strum(serialize_all = \"lowercase\")]\npub(crate) enum ReFunctions {\n    /// `re.compile(pattern, flags=0)` — compile a pattern into a `re.Pattern` object.\n    Compile,\n    /// `re.search(pattern, string, flags=0)` — find first match anywhere in the string.\n    Search,\n    /// `re.match(pattern, string, flags=0)` — match anchored at the start.\n    Match,\n    /// `re.fullmatch(pattern, string, flags=0)` — match the entire string.\n    Fullmatch,\n    /// `re.findall(pattern, string, flags=0)` — return all non-overlapping matches.\n    Findall,\n    /// `re.sub(pattern, repl, string, count=0, flags=0)` — substitute matches.\n    Sub,\n    /// `re.split(pattern, string, maxsplit=0, flags=0)` — split string by pattern.\n    Split,\n    /// `re.finditer(pattern, string, flags=0)` — return iterator over all matches.\n    Finditer,\n    /// `re.escape(pattern)` — escape all non-alphanumeric characters in pattern.\n    Escape,\n}\n\n/// Creates the `re` module and allocates it on the heap.\n///\n/// The module provides regex functions (`compile`, `search`, `match`, `fullmatch`,\n/// `findall`, `sub`) and flag constants (`IGNORECASE`, `MULTILINE`, `DOTALL`).\n///\n/// # Returns\n/// A `HeapId` pointing to the newly allocated module.\n///\n/// # Panics\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Re);\n\n    // Functions\n    module.set_attr(\n        StaticStrings::Compile,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Compile)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Search,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Search)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Match,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Match)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Fullmatch,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Fullmatch)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Findall,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Findall)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Sub,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Sub)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Split,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Split)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Finditer,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Finditer)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::Escape,\n        Value::ModuleFunction(ModuleFunctions::Re(ReFunctions::Escape)),\n        vm,\n    );\n\n    // Flag constants\n    module.set_attr(StaticStrings::NoFlag, Value::Int(i64::from(NOFLAG)), vm);\n    module.set_attr(StaticStrings::Ignorecase, Value::Int(i64::from(IGNORECASE)), vm);\n    module.set_attr(StaticStrings::I, Value::Int(i64::from(IGNORECASE)), vm);\n    module.set_attr(StaticStrings::MultilineFlag, Value::Int(i64::from(MULTILINE)), vm);\n    module.set_attr(StaticStrings::M, Value::Int(i64::from(MULTILINE)), vm);\n    module.set_attr(StaticStrings::DotallFlag, Value::Int(i64::from(DOTALL)), vm);\n    module.set_attr(StaticStrings::S, Value::Int(i64::from(DOTALL)), vm);\n    module.set_attr(StaticStrings::AsciiFlag, Value::Int(i64::from(ASCII)), vm);\n    module.set_attr(StaticStrings::A, Value::Int(i64::from(ASCII)), vm);\n\n    // Exception types\n    module.set_attr(\n        StaticStrings::PatternError,\n        Value::Builtin(Builtins::ExcType(ExcType::RePatternError)),\n        vm,\n    );\n    // `re.error` is the historical alias for `re.PatternError` (still widely used)\n    module.set_attr(\n        StaticStrings::Error,\n        Value::Builtin(Builtins::ExcType(ExcType::RePatternError)),\n        vm,\n    );\n\n    // Constructed types\n    module.set_attr(\n        StaticStrings::PatternClass,\n        Value::Builtin(Builtins::Type(Type::RePattern)),\n        vm,\n    );\n    module.set_attr(\n        StaticStrings::MatchClass,\n        Value::Builtin(Builtins::Type(Type::ReMatch)),\n        vm,\n    );\n\n    vm.heap.allocate(HeapData::Module(module))\n}\n\n/// Dispatches a call to a `re` module function.\n///\n/// Extracts arguments, compiles patterns as needed, and delegates to the appropriate\n/// `RePattern` method. All functions return `CallResult::Value` since regex\n/// operations don't need host involvement.\npub(super) fn call(\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n    function: ReFunctions,\n    args: ArgValues,\n) -> RunResult<CallResult> {\n    match function {\n        ReFunctions::Compile => call_compile(vm, args).map(CallResult::Value),\n        ReFunctions::Search => call_search(vm, args).map(CallResult::Value),\n        ReFunctions::Match => call_match(vm, args).map(CallResult::Value),\n        ReFunctions::Fullmatch => call_fullmatch(vm, args).map(CallResult::Value),\n        ReFunctions::Findall => call_findall(vm, args).map(CallResult::Value),\n        ReFunctions::Sub => call_sub(vm, args).map(CallResult::Value),\n        ReFunctions::Split => call_split(vm, args).map(CallResult::Value),\n        ReFunctions::Finditer => call_finditer(vm, args).map(CallResult::Value),\n        ReFunctions::Escape => call_escape(vm, args).map(CallResult::Value),\n    }\n}\n\n/// `re.compile(pattern, flags=0)` — compile a regular expression pattern.\n///\n/// Returns a `re.Pattern` object that can be reused for multiple match operations.\n/// The pattern is compiled once and stored, avoiding recompilation overhead.\nfn call_compile(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pattern_val, flags) = extract_pattern_and_flags(args, \"re.compile\", vm)?;\n    let compiled = RePattern::compile(pattern_val, flags)?;\n    Ok(Value::Ref(vm.heap.allocate(HeapData::RePattern(Box::new(compiled)))?))\n}\n\n/// `re.search(pattern, string, flags=0)` — scan through string looking for a match.\n///\n/// Compiles the pattern, then delegates to `RePattern::search`. Returns a `re.Match`\n/// object on success, or `None` if no position in the string matches.\nfn call_search(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pattern, text, flags) = extract_pattern_string_flags(args, \"re.search\", vm)?;\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.search(&text, vm.heap)\n}\n\n/// `re.match(pattern, string, flags=0)` — match at the beginning of the string.\n///\n/// Compiles the pattern, then delegates to `RePattern::match_start`. Returns a `re.Match`\n/// object if the pattern matches at position 0, or `None` otherwise.\nfn call_match(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pattern, text, flags) = extract_pattern_string_flags(args, \"re.match\", vm)?;\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.match_start(&text, vm.heap)\n}\n\n/// `re.fullmatch(pattern, string, flags=0)` — match the entire string.\n///\n/// Compiles the pattern, then delegates to `RePattern::fullmatch`. Returns a `re.Match`\n/// object if the pattern matches the whole string, or `None` otherwise.\nfn call_fullmatch(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pattern, text, flags) = extract_pattern_string_flags(args, \"re.fullmatch\", vm)?;\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.fullmatch(&text, vm.heap)\n}\n\n/// `re.findall(pattern, string, flags=0)` — find all non-overlapping matches.\n///\n/// Compiles the pattern, then delegates to `RePattern::findall`. Returns a list of\n/// strings or tuples depending on the number of capture groups (matching CPython semantics).\nfn call_findall(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pattern, text, flags) = extract_pattern_string_flags(args, \"re.findall\", vm)?;\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.findall(&text, vm.heap)\n}\n\n/// `re.sub(pattern, repl, string, count=0, flags=0)` — substitute matches with a replacement.\n///\n/// Compiles the pattern, then delegates to `RePattern::sub`. Replaces occurrences of the\n/// pattern with the replacement string. When `count` is 0, all matches are replaced.\n/// Supports both positional and keyword arguments for `count` and `flags`.\nfn call_sub(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pos, kwargs) = args.into_parts();\n    defer_drop_mut!(pos, vm);\n    let kwargs = kwargs.into_iter();\n    defer_drop_mut!(kwargs, vm);\n\n    let Some(pattern_val) = pos.next() else {\n        return Err(ExcType::type_error(\"re.sub() missing required argument: 'pattern'\"));\n    };\n    defer_drop!(pattern_val, vm);\n\n    let Some(repl_val) = pos.next() else {\n        return Err(ExcType::type_error(\"re.sub() missing required argument: 'repl'\"));\n    };\n    defer_drop!(repl_val, vm);\n\n    let Some(string_val) = pos.next() else {\n        return Err(ExcType::type_error(\"re.sub() missing required argument: 'string'\"));\n    };\n    defer_drop!(string_val, vm);\n\n    // Extract count and flags from remaining positional args\n    let pos_count = pos.next();\n    let pos_flags = pos.next();\n\n    if let Some(extra) = pos.next() {\n        extra.drop_with_heap(vm);\n        return Err(ExcType::type_error(\"re.sub() takes at most 5 positional arguments\"));\n    }\n\n    // Extract count and flags from kwargs (if not given positionally)\n    let (mut kw_count, mut kw_flags): (Option<Value>, Option<Value>) = (None, None);\n    for (key, value) in kwargs {\n        defer_drop!(key, vm);\n        let Some(keyword_name) = key.as_either_str(vm.heap) else {\n            value.drop_with_heap(vm);\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n        let key_str = keyword_name.as_str(vm.interns);\n        match key_str {\n            \"count\" => {\n                if pos_count.is_some() {\n                    value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(\"re.sub() got multiple values for argument 'count'\"));\n                }\n                kw_count.replace(value).drop_with_heap(vm);\n            }\n            \"flags\" => {\n                if pos_flags.is_some() {\n                    value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(\"re.sub() got multiple values for argument 'flags'\"));\n                }\n                kw_flags.replace(value).drop_with_heap(vm);\n            }\n            _ => {\n                value.drop_with_heap(vm);\n                return Err(ExcType::type_error(format!(\n                    \"'{key_str}' is an invalid keyword argument for re.sub()\"\n                )));\n            }\n        }\n    }\n\n    let count_val = pos_count.or(kw_count);\n    let flags_val = pos_flags.or(kw_flags);\n\n    #[expect(\n        clippy::cast_sign_loss,\n        clippy::cast_possible_truncation,\n        reason = \"n is checked non-negative above\"\n    )]\n    let count = match count_val {\n        Some(Value::Int(n)) if n >= 0 => n as usize,\n        Some(Value::Bool(b)) => usize::from(b),\n        Some(Value::Int(_)) => {\n            // Negative count — return original string unchanged\n            let _flags = extract_flags(flags_val, vm.heap)?;\n            let text = value_to_str(string_val, vm.heap, vm.interns)?.into_owned();\n            let s = Str::new(text);\n            return Ok(Value::Ref(vm.heap.allocate(HeapData::Str(s))?));\n        }\n        Some(other) => {\n            let t = other.py_type(vm.heap);\n            other.drop_with_heap(vm);\n            return Err(ExcType::type_error(format!(\n                \"'{t}' object cannot be interpreted as an integer for 'count' argument\"\n            )));\n        }\n        None => 0,\n    };\n\n    let flags = extract_flags(flags_val, vm.heap)?;\n\n    let pattern = value_to_str(pattern_val, vm.heap, vm.interns)?.into_owned();\n\n    // Check that repl is a string — callable replacement is not supported\n    if !repl_val.is_str(vm.heap) {\n        return Err(ExcType::type_error(\n            \"callable replacement is not yet supported in re.sub()\",\n        ));\n    }\n    let repl = value_to_str(repl_val, vm.heap, vm.interns)?.into_owned();\n    let text = value_to_str(string_val, vm.heap, vm.interns)?.into_owned();\n\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.sub(&repl, &text, count, vm.heap)\n}\n\n/// `re.split(pattern, string, maxsplit=0, flags=0)` — split string by pattern occurrences.\n///\n/// Returns a list of strings. If `maxsplit` is non-zero, at most `maxsplit` splits occur\n/// and the remainder of the string is returned as the final list element.\n/// Supports both positional and keyword arguments for `maxsplit` and `flags`.\nfn call_split(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pos, kwargs) = args.into_parts();\n    defer_drop_mut!(pos, vm);\n    let kwargs = kwargs.into_iter();\n    defer_drop_mut!(kwargs, vm);\n\n    let Some(pattern_val) = pos.next() else {\n        return Err(ExcType::type_error(\"re.split() missing required argument: 'pattern'\"));\n    };\n    defer_drop!(pattern_val, vm);\n\n    let Some(string_val) = pos.next() else {\n        return Err(ExcType::type_error(\"re.split() missing required argument: 'string'\"));\n    };\n    defer_drop!(string_val, vm);\n\n    let pos_maxsplit = pos.next();\n    let pos_flags = pos.next();\n\n    if let Some(extra) = pos.next() {\n        extra.drop_with_heap(vm);\n        return Err(ExcType::type_error(\"re.split() takes at most 4 positional arguments\"));\n    }\n\n    let (mut kw_maxsplit, mut kw_flags): (Option<Value>, Option<Value>) = (None, None);\n    for (key, value) in kwargs {\n        defer_drop!(key, vm);\n        let Some(keyword_name) = key.as_either_str(vm.heap) else {\n            value.drop_with_heap(vm);\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n        let key_str = keyword_name.as_str(vm.interns);\n        match key_str {\n            \"maxsplit\" => {\n                if pos_maxsplit.is_some() {\n                    value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(\n                        \"re.split() got multiple values for argument 'maxsplit'\",\n                    ));\n                }\n                kw_maxsplit.replace(value).drop_with_heap(vm);\n            }\n            \"flags\" => {\n                if pos_flags.is_some() {\n                    value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(\n                        \"re.split() got multiple values for argument 'flags'\",\n                    ));\n                }\n                kw_flags.replace(value).drop_with_heap(vm);\n            }\n            _ => {\n                value.drop_with_heap(vm);\n                return Err(ExcType::type_error(format!(\n                    \"'{key_str}' is an invalid keyword argument for re.split()\"\n                )));\n            }\n        }\n    }\n\n    let maxsplit = extract_maxsplit(pos_maxsplit.or(kw_maxsplit), vm.heap)?;\n    let flags = extract_flags(pos_flags.or(kw_flags), vm.heap)?;\n\n    let pattern = value_to_str(pattern_val, vm.heap, vm.interns)?.into_owned();\n    let text = value_to_str(string_val, vm.heap, vm.interns)?.into_owned();\n\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.split(&text, maxsplit, vm.heap)\n}\n\n/// `re.finditer(pattern, string, flags=0)` — return all matches as a list.\n///\n/// Eagerly collects all match objects into a list. When the user iterates with\n/// `for m in re.finditer(...)`, the VM's `GetIter` opcode handles iteration\n/// over the returned list automatically.\nfn call_finditer(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let (pattern, text, flags) = extract_pattern_string_flags(args, \"re.finditer\", vm)?;\n    let compiled = RePattern::compile(pattern, flags)?;\n    compiled.finditer(&text, vm.heap)\n}\n\n/// `re.escape(pattern)` — escape special regex characters in a string.\n///\n/// Returns a string with all regex metacharacters and whitespace prefixed with\n/// a backslash. Only characters that have special meaning in regex patterns are\n/// escaped, matching CPython 3.7+ behavior.\n///\n/// Escaped characters: `\\t \\n \\v \\f \\r   # $ & ( ) * + - . ? [ \\ ] ^ { | } ~`\nfn call_escape(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n    let arg = args.get_one_arg(\"re.escape\", vm.heap)?;\n    defer_drop!(arg, vm);\n    let text = value_to_str(arg, vm.heap, vm.interns)?.into_owned();\n\n    let mut result = String::with_capacity(text.len() * 2);\n    for c in text.chars() {\n        if should_escape(c) {\n            result.push('\\\\');\n        }\n        result.push(c);\n    }\n\n    let s = Str::new(result);\n    Ok(Value::Ref(vm.heap.allocate(HeapData::Str(s))?))\n}\n\n/// Returns whether a character should be escaped by `re.escape()`.\n///\n/// Matches CPython's `_special_chars_map` — only regex metacharacters and whitespace.\nfn should_escape(c: char) -> bool {\n    matches!(\n        c,\n        '\\t' | '\\n'\n            | '\\x0b'\n            | '\\x0c'\n            | '\\r'\n            | ' '\n            | '#'\n            | '$'\n            | '&'\n            | '('\n            | ')'\n            | '*'\n            | '+'\n            | '-'\n            | '.'\n            | '?'\n            | '['\n            | '\\\\'\n            | ']'\n            | '^'\n            | '{'\n            | '|'\n            | '}'\n            | '~'\n    )\n}\n\n/// Extracts a `maxsplit` value from an optional `Value`.\n///\n/// Returns 0 if not provided. Negative values are treated as 0 (split all).\nfn extract_maxsplit(val: Option<Value>, heap: &mut Heap<impl ResourceTracker>) -> RunResult<usize> {\n    match val {\n        None => Ok(0),\n        Some(Value::Int(n)) if n <= 0 => Ok(0),\n        #[expect(\n            clippy::cast_sign_loss,\n            clippy::cast_possible_truncation,\n            reason = \"n is checked positive above\"\n        )]\n        Some(Value::Int(n)) => Ok(n as usize),\n        Some(Value::Bool(b)) => Ok(usize::from(b)),\n        Some(other) => {\n            let t = other.py_type(heap);\n            other.drop_with_heap(heap);\n            Err(ExcType::type_error(format!(\"expected int for maxsplit, not {t}\")))\n        }\n    }\n}\n\n/// Extracts pattern string and optional flags from arguments for `re.compile()`.\n///\n/// Accepts 1 or 2 positional arguments: `(pattern)` or `(pattern, flags)`.\n/// The pattern must be a string, and flags must be a non-negative integer.\nfn extract_pattern_and_flags(\n    args: ArgValues,\n    func_name: &str,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(String, u16)> {\n    let (pattern_val, flags_val) = args.get_one_two_args(func_name, vm.heap)?;\n    defer_drop!(pattern_val, vm);\n\n    let pattern = value_to_str(pattern_val, vm.heap, vm.interns)?.into_owned();\n    let flags = extract_flags(flags_val, vm.heap)?;\n\n    Ok((pattern, flags))\n}\n\n/// Extracts a flags value from an optional `Value`, validating it is a non-negative integer\n/// that fits in a `u16`.\nfn extract_flags(flags_val: Option<Value>, heap: &mut Heap<impl ResourceTracker>) -> RunResult<u16> {\n    match flags_val {\n        Some(Value::Int(n)) => {\n            u16::try_from(n).map_err(|_| ExcType::type_error(\"flags must be a non-negative integer\"))\n        }\n        // CPython treats bool as int subclass: True=1, False=0.\n        Some(Value::Bool(b)) => Ok(u16::from(b)),\n        Some(other) => {\n            let t = other.py_type(heap);\n            other.drop_with_heap(heap);\n            Err(ExcType::type_error(format!(\"expected int for flags, not {t}\")))\n        }\n        None => Ok(0),\n    }\n}\n\n/// Extracts pattern, string, and optional flags for `re.search()`, `re.match()`,\n/// `re.fullmatch()`, and `re.findall()`.\n///\n/// Accepts 2 or 3 positional arguments: `(pattern, string)` or `(pattern, string, flags)`.\nfn extract_pattern_string_flags(\n    args: ArgValues,\n    func_name: &str,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(String, Cow<'static, str>, u16)> {\n    let pos = args.into_pos_only(func_name, vm.heap)?;\n    defer_drop_mut!(pos, vm);\n\n    let Some(pattern_val) = pos.next() else {\n        return Err(ExcType::type_error(format!(\n            \"{func_name}() missing required argument: 'pattern'\"\n        )));\n    };\n    defer_drop!(pattern_val, vm);\n\n    let Some(string_val) = pos.next() else {\n        return Err(ExcType::type_error(format!(\n            \"{func_name}() missing required argument: 'string'\"\n        )));\n    };\n    defer_drop!(string_val, vm);\n\n    let flags = extract_flags(pos.next(), vm.heap)?;\n\n    if let Some(extra) = pos.next() {\n        extra.drop_with_heap(vm);\n        return Err(ExcType::type_error(format!(\n            \"{func_name}() takes at most 3 positional arguments\"\n        )));\n    }\n\n    let pattern = value_to_str(pattern_val, vm.heap, vm.interns)?.into_owned();\n    let text = value_to_str(string_val, vm.heap, vm.interns)?.into_owned();\n\n    Ok((pattern, Cow::Owned(text), flags))\n}\n"
  },
  {
    "path": "crates/monty/src/modules/sys.rs",
    "content": "//! Implementation of the `sys` module.\n//!\n//! Provides a minimal implementation of Python's `sys` module with:\n//! - `version`: Python version string (e.g., \"3.14.0 (Monty)\")\n//! - `version_info`: Named tuple (3, 14, 0, 'final', 0)\n//! - `platform`: Platform identifier (\"monty\")\n//! - `stdout`: Marker for standard output (no real functionality)\n//! - `stderr`: Marker for standard error (no real functionality)\n\nuse crate::{\n    bytecode::VM,\n    heap::{HeapData, HeapId},\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    types::{Module, NamedTuple},\n    value::{Marker, Value},\n};\n\n/// Creates the `sys` module and allocates it on the heap.\n///\n/// Returns a HeapId pointing to the newly allocated module.\n///\n/// # Panics\n///\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Sys);\n\n    // sys.platform\n    module.set_attr(StaticStrings::Platform, StaticStrings::Monty.into(), vm);\n\n    // sys.stdout / sys.stderr - markers for standard output/error\n    module.set_attr(StaticStrings::Stdout, Value::Marker(Marker(StaticStrings::Stdout)), vm);\n    module.set_attr(StaticStrings::Stderr, Value::Marker(Marker(StaticStrings::Stderr)), vm);\n\n    // sys.version\n    module.set_attr(StaticStrings::Version, StaticStrings::MontyVersionString.into(), vm);\n    // sys.version_info - named tuple (major=3, minor=14, micro=0, releaselevel='final', serial=0)\n    let version_info = NamedTuple::new(\n        StaticStrings::SysVersionInfo,\n        vec![\n            StaticStrings::Major.into(),\n            StaticStrings::Minor.into(),\n            StaticStrings::Micro.into(),\n            StaticStrings::Releaselevel.into(),\n            StaticStrings::Serial.into(),\n        ],\n        vec![\n            Value::Int(3),\n            Value::Int(14),\n            Value::Int(0),\n            Value::InternString(StaticStrings::Final.into()),\n            Value::Int(0),\n        ],\n    );\n    let version_info_id = vm.heap.allocate(HeapData::NamedTuple(version_info))?;\n    module.set_attr(StaticStrings::VersionInfo, Value::Ref(version_info_id), vm);\n\n    vm.heap.allocate(HeapData::Module(module))\n}\n"
  },
  {
    "path": "crates/monty/src/modules/typing.rs",
    "content": "//! Implementation of the `typing` module.\n//!\n//! Provides a minimal implementation of Python's `typing` module with:\n//! - `TYPE_CHECKING`: Always False (used for conditional imports)\n//! - Common type hints as `Marker` values (Any, Optional, List, Dict, etc.)\n//!\n//! These markers exist so code that imports typing constructs works correctly,\n//! though Monty doesn't perform static type checking.\n\nuse crate::{\n    bytecode::VM,\n    heap::{HeapData, HeapId},\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    types::Module,\n    value::{Marker, Value},\n};\n\n/// Creates the `typing` module and allocates it on the heap.\n///\n/// Returns a HeapId pointing to the newly allocated module.\n///\n/// # Panics\n///\n/// Panics if the required strings have not been pre-interned during prepare phase.\npub fn create_module(vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {\n    let mut module = Module::new(StaticStrings::Typing);\n\n    // typing.TYPE_CHECKING - always False\n    module.set_attr(StaticStrings::TypeChecking, Value::Bool(false), vm);\n\n    // Export all typing markers as module attributes\n    for ss in MARKER_ATTRS {\n        module.set_attr(*ss, Value::Marker(Marker(*ss)), vm);\n    }\n\n    vm.heap.allocate(HeapData::Module(module))\n}\n\n/// Typing marker attributes exported by this module.\n///\n/// Each marker wraps its corresponding `StaticStrings` variant as both the\n/// attribute name and the marker value.\nconst MARKER_ATTRS: &[StaticStrings] = &[\n    StaticStrings::Any,\n    StaticStrings::Optional,\n    StaticStrings::UnionType,\n    StaticStrings::ListType,\n    StaticStrings::DictType,\n    StaticStrings::TupleType,\n    StaticStrings::SetType,\n    StaticStrings::FrozenSet,\n    StaticStrings::Callable,\n    StaticStrings::Type,\n    StaticStrings::Sequence,\n    StaticStrings::Mapping,\n    StaticStrings::Iterable,\n    StaticStrings::IteratorType,\n    StaticStrings::Generator,\n    StaticStrings::ClassVar,\n    StaticStrings::FinalType,\n    StaticStrings::Literal,\n    StaticStrings::TypeVar,\n    StaticStrings::Generic,\n    StaticStrings::Protocol,\n    StaticStrings::Annotated,\n    StaticStrings::SelfType,\n    StaticStrings::Never,\n    StaticStrings::NoReturn,\n];\n"
  },
  {
    "path": "crates/monty/src/namespace.rs",
    "content": "/// Unique identifier for variable slots in namespaces (globals and function locals).\n///\n/// Used by the bytecode compiler to emit slot indices for variable access.\n/// The VM uses these indices to read/write values in the globals vector\n/// or the stack-inlined locals region.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]\npub(crate) struct NamespaceId(u32);\n\nimpl NamespaceId {\n    pub fn new(index: usize) -> Self {\n        Self(index.try_into().expect(\"Invalid namespace id\"))\n    }\n\n    /// Returns the raw index value.\n    ///\n    /// Used by the bytecode compiler to emit slot indices for variable access.\n    #[inline]\n    pub fn index(self) -> usize {\n        self.0 as usize\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/object.rs",
    "content": "use std::{\n    borrow::Cow,\n    fmt::{self, Write},\n    hash::{Hash, Hasher},\n};\n\nuse ahash::AHashSet;\nuse indexmap::IndexMap;\nuse num_bigint::BigInt;\nuse num_traits::Zero;\n\nuse crate::{\n    builtins::{Builtins, BuiltinsFunctions},\n    bytecode::VM,\n    exception_private::{ExcType, SimpleException},\n    heap::{HeapData, HeapId},\n    resource::{ResourceError, ResourceTracker},\n    types::{\n        LongInt, NamedTuple, Path, PyTrait, Type, allocate_tuple,\n        bytes::{Bytes, bytes_repr},\n        dict::Dict,\n        list::List,\n        set::{FrozenSet, Set},\n        str::{Str, StringRepr, string_repr_fmt},\n    },\n    value::{EitherStr, Value},\n};\n\n/// A Python value that can be passed to or returned from the interpreter.\n///\n/// This is the public-facing type for Python values. It owns all its data and can be\n/// freely cloned, serialized, or stored. Unlike the internal `Value` type, `MontyObject`\n/// does not require a heap for operations.\n///\n/// # Input vs Output Variants\n///\n/// Most variants can be used both as inputs (passed to `Executor::run()`) and outputs\n/// (returned from execution). However:\n/// - `Repr` is output-only: represents values that have no direct `MontyObject` mapping\n/// - `Exception` can be used as input (to raise) or output (when code raises)\n///\n/// # Hashability\n///\n/// Only immutable variants (`None`, `Ellipsis`, `Bool`, `Int`, `Float`, `String`, `Bytes`)\n/// implement `Hash`. Attempting to hash mutable variants (`List`, `Dict`) will panic.\n///\n/// # JSON Serialization\n///\n/// `MontyObject` supports JSON serialization with natural mappings:\n///\n/// **Bidirectional (can serialize and deserialize):**\n/// - `None` ↔ JSON `null`\n/// - `Bool` ↔ JSON `true`/`false`\n/// - `Int` ↔ JSON integer\n/// - `Float` ↔ JSON float\n/// - `String` ↔ JSON string\n/// - `List` ↔ JSON array\n/// - `Dict` ↔ JSON object (keys must be interns)\n///\n/// **Output-only (serialize only, cannot deserialize from JSON):**\n/// - `Ellipsis` → `{\"$ellipsis\": true}`\n/// - `Tuple` → `{\"$tuple\": [...]}`\n/// - `Bytes` → `{\"$bytes\": [...]}`\n/// - `Exception` → `{\"$exception\": {\"type\": \"...\", \"arg\": \"...\"}}`\n/// - `Repr` → `{\"$repr\": \"...\"}`\n///\n/// # Binary Serialization\n///\n/// For binary serialization (e.g., with postcard), `MontyObject` uses derived serde\n/// with internally tagged format. This differs from the natural JSON format.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum MontyObject {\n    /// Python's `Ellipsis` singleton (`...`).\n    Ellipsis,\n    /// Python's `None` singleton.\n    None,\n    /// Python boolean (`True` or `False`).\n    Bool(bool),\n    /// Python integer (64-bit signed).\n    Int(i64),\n    /// Python arbitrary-precision integer (larger than i64).\n    BigInt(BigInt),\n    /// Python float (64-bit IEEE 754).\n    Float(f64),\n    /// Python string (UTF-8).\n    String(String),\n    /// Python bytes object.\n    Bytes(Vec<u8>),\n    /// Python list (mutable sequence).\n    List(Vec<Self>),\n    /// Python tuple (immutable sequence).\n    Tuple(Vec<Self>),\n    /// Python named tuple (immutable sequence with named fields).\n    ///\n    /// Named tuples behave like tuples but also support attribute access by field name.\n    /// The type_name is used in repr (e.g., \"os.stat_result\"), and field_names provides\n    /// the attribute names for each position.\n    NamedTuple {\n        /// Type name for repr (e.g., \"os.stat_result\").\n        type_name: String,\n        /// Field names in order.\n        field_names: Vec<String>,\n        /// Values in order (same length as field_names).\n        values: Vec<Self>,\n    },\n    /// Python dictionary (insertion-ordered mapping).\n    Dict(DictPairs),\n    /// Python set (mutable, unordered collection of unique elements).\n    Set(Vec<Self>),\n    /// Python frozenset (immutable, unordered collection of unique elements).\n    FrozenSet(Vec<Self>),\n    /// Python exception with type and optional message argument.\n    Exception {\n        /// The exception type (e.g., `ValueError`, `TypeError`).\n        exc_type: ExcType,\n        /// Optional string argument passed to the exception constructor.\n        arg: Option<String>,\n    },\n    /// A Python type object (e.g., `int`, `str`, `list`).\n    ///\n    /// Returned by the `type()` builtin and can be compared with other types.\n    Type(Type),\n    BuiltinFunction(BuiltinsFunctions),\n    /// Python `pathlib.Path` object (or technically a `PurePosixPath`).\n    ///\n    /// Represents a filesystem path. Can be used both as input (from host) and output.\n    Path(String),\n    /// A dataclass instance with class name, field names, attributes, and mutability.\n    ///\n    /// Method calls are detected lazily at runtime: when `call_attr` is invoked\n    /// on a dataclass and the attribute name is not found in `attrs`, it is\n    /// dispatched as a `MethodCall` to the host (provided the name is public).\n    Dataclass {\n        /// The class name (e.g., \"Point\", \"User\").\n        name: String,\n        /// Identifier of the type, from `id(type(dc))` in python.\n        type_id: u64,\n        /// Declared field names in definition order (for repr).\n        field_names: Vec<String>,\n        /// All attribute name -> value mapping (includes fields and extra attrs).\n        attrs: DictPairs,\n        /// Whether this dataclass instance is immutable.\n        frozen: bool,\n    },\n    /// An external function provided by the host.\n    ///\n    /// Returned by the host in response to a `NameLookup` to provide a callable\n    /// that the VM can invoke. When called, the VM yields `FunctionCall` to the host.\n    Function {\n        /// The function name (used for repr, error messages, and function call identification).\n        name: String,\n        /// Optional docstring for the function.\n        docstring: Option<String>,\n    },\n    /// Fallback for values that cannot be represented as other variants.\n    ///\n    /// Contains the `repr()` string of the original value.\n    ///\n    /// This is output-only and cannot be used as an input to `Executor::run()`.\n    Repr(String),\n    /// Represents a cycle detected during Value-to-MontyObject conversion.\n    ///\n    /// When converting cyclic structures (e.g., `a = []; a.append(a)`), this variant\n    /// is used to break the infinite recursion. Contains the heap ID and the type-specific\n    /// placeholder string (e.g., `\"[...]\"` for lists, `\"{...}\"` for dicts).\n    ///\n    /// This is output-only and cannot be used as an input to `Executor::run()`.\n    Cycle(HeapId, String),\n}\n\nimpl fmt::Display for MontyObject {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::String(s) => f.write_str(s),\n            Self::Cycle(_, placeholder) => f.write_str(placeholder),\n            Self::Type(t) => write!(f, \"<class '{t}'>\"),\n            Self::Function { name, .. } => write!(f, \"<function '{name}' external>\"),\n            _ => self.repr_fmt(f),\n        }\n    }\n}\n\nimpl MontyObject {\n    /// Converts a `Value` into a `MontyObject`, properly handling reference counting.\n    ///\n    /// Takes ownership of the `Value`, extracts its content to create a MontyObject,\n    /// then properly drops the Value via `drop_with_heap` to maintain reference counting.\n    ///\n    /// The `interns` parameter is used to look up interned string/bytes content.\n    pub(crate) fn new(value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Self {\n        let py_obj = Self::from_value(&value, vm);\n        value.drop_with_heap(vm.heap);\n        py_obj\n    }\n\n    /// Creates a new `MontyObject` from something that can be converted into a `DictPairs`.\n    pub fn dict(dict: impl Into<DictPairs>) -> Self {\n        Self::Dict(dict.into())\n    }\n\n    /// Converts this `MontyObject` into an `Value`, allocating on the heap if needed.\n    ///\n    /// Immediate values (None, Bool, Int, Float, Ellipsis, Exception) are created directly.\n    /// Heap-allocated values (String, Bytes, List, Tuple, Dict) are allocated\n    /// via the heap and wrapped in `Value::Ref`.\n    ///\n    /// # Errors\n    /// Returns `InvalidInputError` if called on the `Repr` variant,\n    /// as it is only valid as an output from code execution, not as an input.\n    pub(crate) fn to_value(self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<Value, InvalidInputError> {\n        match self {\n            Self::Ellipsis => Ok(Value::Ellipsis),\n            Self::None => Ok(Value::None),\n            Self::Bool(b) => Ok(Value::Bool(b)),\n            Self::Int(i) => Ok(Value::Int(i)),\n            Self::BigInt(bi) => Ok(LongInt::new(bi).into_value(vm.heap)?),\n            Self::Float(f) => Ok(Value::Float(f)),\n            Self::String(s) => Ok(Value::Ref(vm.heap.allocate(HeapData::Str(Str::new(s)))?)),\n            Self::Bytes(b) => Ok(Value::Ref(vm.heap.allocate(HeapData::Bytes(Bytes::new(b)))?)),\n            Self::List(items) => {\n                let values: Vec<Value> = items\n                    .into_iter()\n                    .map(|item| item.to_value(vm))\n                    .collect::<Result<_, _>>()?;\n                Ok(Value::Ref(vm.heap.allocate(HeapData::List(List::new(values)))?))\n            }\n            Self::Tuple(items) => {\n                let values = items\n                    .into_iter()\n                    .map(|item| item.to_value(vm))\n                    .collect::<Result<_, _>>()?;\n                allocate_tuple(values, vm.heap).map_err(InvalidInputError::Resource)\n            }\n            Self::NamedTuple {\n                type_name,\n                field_names,\n                values,\n            } => {\n                let values: Vec<Value> = values\n                    .into_iter()\n                    .map(|item| item.to_value(vm))\n                    .collect::<Result<_, _>>()?;\n                let field_name_strs: Vec<EitherStr> = field_names.into_iter().map(Into::into).collect();\n                let nt = NamedTuple::new(type_name, field_name_strs, values);\n                Ok(Value::Ref(vm.heap.allocate(HeapData::NamedTuple(nt))?))\n            }\n            Self::Dict(map) => {\n                let pairs: Result<Vec<(Value, Value)>, InvalidInputError> = map\n                    .into_iter()\n                    .map(|(k, v)| Ok((k.to_value(vm)?, v.to_value(vm)?)))\n                    .collect();\n                let dict = Dict::from_pairs(pairs?, vm)\n                    .map_err(|_| InvalidInputError::invalid_type(\"unhashable dict keys\"))?;\n                Ok(Value::Ref(vm.heap.allocate(HeapData::Dict(dict))?))\n            }\n            Self::Set(items) => {\n                let mut set = Set::new();\n                for item in items {\n                    let value = item.to_value(vm)?;\n                    set.add(value, vm)\n                        .map_err(|_| InvalidInputError::invalid_type(\"unhashable set element\"))?;\n                }\n                Ok(Value::Ref(vm.heap.allocate(HeapData::Set(set))?))\n            }\n            Self::FrozenSet(items) => {\n                let mut set = Set::new();\n                for item in items {\n                    let value = item.to_value(vm)?;\n                    set.add(value, vm)\n                        .map_err(|_| InvalidInputError::invalid_type(\"unhashable frozenset element\"))?;\n                }\n                // Convert to frozenset by extracting storage\n                let frozenset = FrozenSet::from_set(set);\n                Ok(Value::Ref(vm.heap.allocate(HeapData::FrozenSet(frozenset))?))\n            }\n            Self::Exception { exc_type, arg } => {\n                let exc = SimpleException::new(exc_type, arg);\n                Ok(Value::Ref(vm.heap.allocate(HeapData::Exception(exc))?))\n            }\n            Self::Dataclass {\n                name,\n                type_id,\n                field_names,\n                attrs,\n                frozen,\n            } => {\n                use crate::types::Dataclass;\n                // Convert attrs to Dict\n                let pairs: Result<Vec<(Value, Value)>, InvalidInputError> = attrs\n                    .into_iter()\n                    .map(|(k, v)| Ok((k.to_value(vm)?, v.to_value(vm)?)))\n                    .collect();\n                let dict = Dict::from_pairs(pairs?, vm)\n                    .map_err(|_| InvalidInputError::invalid_type(\"unhashable dataclass attr keys\"))?;\n                let dc = Dataclass::new(name, type_id, field_names, dict, frozen);\n                Ok(Value::Ref(vm.heap.allocate(HeapData::Dataclass(dc))?))\n            }\n            Self::Path(s) => Ok(Value::Ref(vm.heap.allocate(HeapData::Path(Path::new(s)))?)),\n            Self::Type(t) => Ok(Value::Builtin(Builtins::Type(t))),\n            Self::BuiltinFunction(f) => Ok(Value::Builtin(Builtins::Function(f))),\n            Self::Function { name, .. } => {\n                // Try to intern the function name. If the name is already interned\n                // (common case: the function has the same name as the variable it was\n                // assigned to), use the lightweight `Value::ExtFunction(StringId)`.\n                // Otherwise, allocate a `HeapData::ExtFunction(String)` on the heap.\n                if let Some(string_id) = vm.interns.get_string_id_by_name(&name) {\n                    Ok(Value::ExtFunction(string_id))\n                } else {\n                    Ok(Value::Ref(vm.heap.allocate(HeapData::ExtFunction(name))?))\n                }\n            }\n            Self::Repr(_) => Err(InvalidInputError::invalid_type(\"'Repr' is not a valid input value\")),\n            Self::Cycle(_, _) => Err(InvalidInputError::invalid_type(\"'Cycle' is not a valid input value\")),\n        }\n    }\n\n    fn from_value(object: &Value, vm: &VM<'_, '_, impl ResourceTracker>) -> Self {\n        let mut visited = AHashSet::new();\n        Self::from_value_inner(object, vm, &mut visited)\n    }\n\n    /// Internal helper for converting Value to MontyObject with cycle detection.\n    ///\n    /// The `visited` set tracks HeapIds we're currently processing. When we encounter\n    /// a HeapId already in the set, we've found a cycle and return `MontyObject::Cycle`\n    /// with an appropriate placeholder string.\n    ///\n    /// Recursion depth is tracked via `heap.incr_recursion_depth_for_repr()`.\n    fn from_value_inner(object: &Value, vm: &VM<'_, '_, impl ResourceTracker>, visited: &mut AHashSet<HeapId>) -> Self {\n        // Check depth limit before processing\n        let Some(token) = vm.heap.incr_recursion_depth_for_repr() else {\n            return Self::Repr(\"<deeply nested>\".to_owned());\n        };\n        crate::defer_drop_immutable_heap!(token, vm);\n        match object {\n            Value::Undefined => panic!(\"Undefined found while converting to MontyObject\"),\n            Value::Ellipsis => Self::Ellipsis,\n            Value::None => Self::None,\n            Value::Bool(b) => Self::Bool(*b),\n            Value::Int(i) => Self::Int(*i),\n            Value::Float(f) => Self::Float(*f),\n            Value::InternString(string_id) => Self::String(vm.interns.get_str(*string_id).to_owned()),\n            Value::InternBytes(bytes_id) => Self::Bytes(vm.interns.get_bytes(*bytes_id).to_owned()),\n            Value::Ref(id) => {\n                // Check for cycle\n                if visited.contains(id) {\n                    // Cycle detected - return appropriate placeholder\n                    return match vm.heap.get(*id) {\n                        HeapData::List(_) => Self::Cycle(*id, \"[...]\".to_owned()),\n                        HeapData::Tuple(_) | HeapData::NamedTuple(_) => Self::Cycle(*id, \"(...)\".to_owned()),\n                        HeapData::Dict(_) => Self::Cycle(*id, \"{...}\".to_owned()),\n                        _ => Self::Cycle(*id, \"...\".to_owned()),\n                    };\n                }\n\n                // Mark this id as being visited\n                visited.insert(*id);\n\n                let result = match vm.heap.get(*id) {\n                    HeapData::Str(s) => Self::String(s.as_str().to_owned()),\n                    HeapData::Bytes(b) => Self::Bytes(b.as_slice().to_owned()),\n                    HeapData::List(list) => Self::List(\n                        list.as_slice()\n                            .iter()\n                            .map(|obj| Self::from_value_inner(obj, vm, visited))\n                            .collect(),\n                    ),\n                    HeapData::Tuple(tuple) => Self::Tuple(\n                        tuple\n                            .as_slice()\n                            .iter()\n                            .map(|obj| Self::from_value_inner(obj, vm, visited))\n                            .collect(),\n                    ),\n                    HeapData::NamedTuple(nt) => Self::NamedTuple {\n                        type_name: nt.name(vm.interns).to_owned(),\n                        field_names: nt\n                            .field_names()\n                            .iter()\n                            .map(|field_name| field_name.as_str(vm.interns).to_owned())\n                            .collect(),\n                        values: nt\n                            .as_vec()\n                            .iter()\n                            .map(|obj| Self::from_value_inner(obj, vm, visited))\n                            .collect(),\n                    },\n                    HeapData::Dict(dict) => Self::Dict(DictPairs(\n                        dict.into_iter()\n                            .map(|(k, v)| {\n                                (\n                                    Self::from_value_inner(k, vm, visited),\n                                    Self::from_value_inner(v, vm, visited),\n                                )\n                            })\n                            .collect(),\n                    )),\n                    HeapData::Set(set) => Self::Set(\n                        set.storage()\n                            .iter()\n                            .map(|obj| Self::from_value_inner(obj, vm, visited))\n                            .collect(),\n                    ),\n                    HeapData::FrozenSet(frozenset) => Self::FrozenSet(\n                        frozenset\n                            .storage()\n                            .iter()\n                            .map(|obj| Self::from_value_inner(obj, vm, visited))\n                            .collect(),\n                    ),\n                    // Cells are internal closure implementation details\n                    HeapData::Cell(cell) => {\n                        // Show the cell's contents\n                        Self::from_value_inner(&cell.0, vm, visited)\n                    }\n                    HeapData::Closure(..) | HeapData::FunctionDefaults(..) => {\n                        Self::Repr(object.py_repr(vm).into_owned())\n                    }\n                    HeapData::Range(range) => {\n                        // Represent Range as a repr string since MontyObject doesn't have a Range variant\n                        let mut s = String::new();\n                        let _ = range.py_repr_fmt(&mut s, vm, visited);\n                        Self::Repr(s)\n                    }\n                    HeapData::Exception(exc) => Self::Exception {\n                        exc_type: exc.exc_type(),\n                        arg: exc.arg().map(ToString::to_string),\n                    },\n                    HeapData::Dataclass(dc) => {\n                        // Convert attrs to DictPairs\n                        let attrs = DictPairs(\n                            dc.attrs()\n                                .into_iter()\n                                .map(|(k, v)| {\n                                    (\n                                        Self::from_value_inner(k, vm, visited),\n                                        Self::from_value_inner(v, vm, visited),\n                                    )\n                                })\n                                .collect(),\n                        );\n                        Self::Dataclass {\n                            name: dc.name(vm.interns).to_owned(),\n                            type_id: dc.type_id(),\n                            field_names: dc.field_names().to_vec(),\n                            attrs,\n                            frozen: dc.is_frozen(),\n                        }\n                    }\n                    HeapData::Iter(_) => {\n                        // Iterators are internal objects - represent as a type string\n                        Self::Repr(\"<iterator>\".to_owned())\n                    }\n                    HeapData::DictKeysView(_) | HeapData::DictItemsView(_) | HeapData::DictValuesView(_) => {\n                        Self::Repr(object.py_repr(vm).into_owned())\n                    }\n                    HeapData::LongInt(li) => Self::BigInt(li.inner().clone()),\n                    HeapData::Module(m) => {\n                        // Modules are represented as a repr string\n                        Self::Repr(format!(\"<module '{}'>\", vm.interns.get_str(m.name())))\n                    }\n                    HeapData::Slice(slice) => {\n                        // Represent Slice as a repr string since MontyObject doesn't have a Slice variant\n                        let mut s = String::new();\n                        let _ = slice.py_repr_fmt(&mut s, vm, visited);\n                        Self::Repr(s)\n                    }\n                    HeapData::Coroutine(coro) => {\n                        // Coroutines are represented as a repr string\n                        let func = vm.interns.get_function(coro.func_id);\n                        let name = vm.interns.get_str(func.name.name_id);\n                        Self::Repr(format!(\"<coroutine object {name}>\"))\n                    }\n                    HeapData::GatherFuture(gather) => {\n                        // GatherFutures are represented as a repr string\n                        Self::Repr(format!(\"<gather({})>\", gather.item_count()))\n                    }\n                    HeapData::Path(path) => Self::Path(path.as_str().to_owned()),\n                    HeapData::RePattern(_) | HeapData::ReMatch(_) => Self::Repr(object.py_repr(vm).into_owned()),\n                    HeapData::ExtFunction(name) => Self::Function {\n                        name: name.clone(),\n                        docstring: None,\n                    },\n                };\n\n                // Remove from visited set after processing\n                visited.remove(id);\n                result\n            }\n            Value::Builtin(Builtins::Type(t)) => Self::Type(*t),\n            Value::Builtin(Builtins::ExcType(e)) => Self::Type(Type::Exception(*e)),\n            Value::Builtin(Builtins::Function(f)) => Self::BuiltinFunction(*f),\n            #[cfg(feature = \"ref-count-panic\")]\n            Value::Dereferenced => panic!(\"Dereferenced found while converting to MontyObject\"),\n            _ => Self::Repr(object.py_repr(vm).into_owned()),\n        }\n    }\n\n    /// Returns the Python `repr()` string for this value.\n    ///\n    /// # Panics\n    /// Could panic if out of memory.\n    #[must_use]\n    pub fn py_repr(&self) -> String {\n        let mut s = String::new();\n        self.repr_fmt(&mut s).expect(\"Unable to format repr display value\");\n        s\n    }\n\n    fn repr_fmt(&self, f: &mut impl Write) -> fmt::Result {\n        match self {\n            Self::Ellipsis => f.write_str(\"Ellipsis\"),\n            Self::None => f.write_str(\"None\"),\n            Self::Bool(true) => f.write_str(\"True\"),\n            Self::Bool(false) => f.write_str(\"False\"),\n            Self::Int(v) => write!(f, \"{v}\"),\n            Self::BigInt(v) => write!(f, \"{v}\"),\n            Self::Float(v) => {\n                let s = v.to_string();\n                f.write_str(&s)?;\n                if !s.contains('.') {\n                    f.write_str(\".0\")?;\n                }\n                Ok(())\n            }\n            Self::String(s) => string_repr_fmt(s, f),\n            Self::Bytes(b) => f.write_str(&bytes_repr(b)),\n            Self::List(l) => {\n                f.write_char('[')?;\n                let mut iter = l.iter();\n                if let Some(first) = iter.next() {\n                    first.repr_fmt(f)?;\n                    for item in iter {\n                        f.write_str(\", \")?;\n                        item.repr_fmt(f)?;\n                    }\n                }\n                f.write_char(']')\n            }\n            Self::Tuple(t) => {\n                f.write_char('(')?;\n                let mut iter = t.iter();\n                if let Some(first) = iter.next() {\n                    first.repr_fmt(f)?;\n                    for item in iter {\n                        f.write_str(\", \")?;\n                        item.repr_fmt(f)?;\n                    }\n                }\n                f.write_char(')')\n            }\n            Self::NamedTuple {\n                type_name,\n                field_names,\n                values,\n            } => {\n                // Format: type_name(field1=value1, field2=value2, ...)\n                f.write_str(type_name)?;\n                f.write_char('(')?;\n                let mut first = true;\n                for (name, value) in field_names.iter().zip(values) {\n                    if !first {\n                        f.write_str(\", \")?;\n                    }\n                    first = false;\n                    f.write_str(name)?;\n                    f.write_char('=')?;\n                    value.repr_fmt(f)?;\n                }\n                f.write_char(')')\n            }\n            Self::Dict(d) => {\n                f.write_char('{')?;\n                let mut iter = d.iter();\n                if let Some((k, v)) = iter.next() {\n                    k.repr_fmt(f)?;\n                    f.write_str(\": \")?;\n                    v.repr_fmt(f)?;\n                    for (k, v) in iter {\n                        f.write_str(\", \")?;\n                        k.repr_fmt(f)?;\n                        f.write_str(\": \")?;\n                        v.repr_fmt(f)?;\n                    }\n                }\n                f.write_char('}')\n            }\n            Self::Set(s) => {\n                if s.is_empty() {\n                    f.write_str(\"set()\")\n                } else {\n                    f.write_char('{')?;\n                    let mut iter = s.iter();\n                    if let Some(first) = iter.next() {\n                        first.repr_fmt(f)?;\n                        for item in iter {\n                            f.write_str(\", \")?;\n                            item.repr_fmt(f)?;\n                        }\n                    }\n                    f.write_char('}')\n                }\n            }\n            Self::FrozenSet(fs) => {\n                f.write_str(\"frozenset(\")?;\n                if !fs.is_empty() {\n                    f.write_char('{')?;\n                    let mut iter = fs.iter();\n                    if let Some(first) = iter.next() {\n                        first.repr_fmt(f)?;\n                        for item in iter {\n                            f.write_str(\", \")?;\n                            item.repr_fmt(f)?;\n                        }\n                    }\n                    f.write_char('}')?;\n                }\n                f.write_char(')')\n            }\n            Self::Exception { exc_type, arg } => {\n                let type_str: &'static str = exc_type.into();\n                write!(f, \"{type_str}(\")?;\n\n                if let Some(arg) = &arg {\n                    string_repr_fmt(arg, f)?;\n                }\n                f.write_char(')')\n            }\n            Self::Dataclass {\n                name,\n                field_names,\n                attrs,\n                ..\n            } => {\n                // Format: ClassName(field1=value1, field2=value2, ...)\n                // Only declared fields are shown, not extra attributes\n                f.write_str(name)?;\n                f.write_char('(')?;\n                let mut first = true;\n                for field_name in field_names {\n                    if !first {\n                        f.write_str(\", \")?;\n                    }\n                    first = false;\n                    f.write_str(field_name)?;\n                    f.write_char('=')?;\n                    // Look up value in attrs\n                    let key = Self::String(field_name.clone());\n                    if let Some(value) = attrs.iter().find(|(k, _)| k == &key).map(|(_, v)| v) {\n                        value.repr_fmt(f)?;\n                    } else {\n                        f.write_str(\"<?>\")?;\n                    }\n                }\n                f.write_char(')')\n            }\n            Self::Path(p) => write!(f, \"PosixPath('{p}')\"),\n            Self::Type(t) => write!(f, \"<class '{t}'>\"),\n            Self::BuiltinFunction(func) => write!(f, \"<built-in function {func}>\"),\n            Self::Function { name, .. } => write!(f, \"<function '{name}' external>\"),\n            Self::Repr(s) => write!(f, \"Repr({})\", StringRepr(s)),\n            Self::Cycle(_, placeholder) => f.write_str(placeholder),\n        }\n    }\n\n    /// Returns `true` if this value is \"truthy\" according to Python's truth testing rules.\n    ///\n    /// In Python, the following values are considered falsy:\n    /// - `None` and `Ellipsis`\n    /// - `False`\n    /// - Zero numeric values (`0`, `0.0`)\n    /// - Empty sequences and collections (`\"\"`, `b\"\"`, `[]`, `()`, `{}`)\n    ///\n    /// All other values are truthy, including `Exception` and `Repr` variants.\n    #[must_use]\n    pub fn is_truthy(&self) -> bool {\n        match self {\n            Self::None => false,\n            Self::Ellipsis => true,\n            Self::Bool(b) => *b,\n            Self::Int(i) => *i != 0,\n            Self::BigInt(bi) => !bi.is_zero(),\n            Self::Float(f) => *f != 0.0,\n            Self::String(s) => !s.is_empty(),\n            Self::Bytes(b) => !b.is_empty(),\n            Self::List(l) => !l.is_empty(),\n            Self::Tuple(t) => !t.is_empty(),\n            Self::NamedTuple { values, .. } => !values.is_empty(),\n            Self::Dict(d) => !d.is_empty(),\n            Self::Set(s) => !s.is_empty(),\n            Self::FrozenSet(fs) => !fs.is_empty(),\n            Self::Exception { .. } => true,\n            Self::Path(_) => true,          // Path instances are always truthy\n            Self::Dataclass { .. } => true, // Dataclass instances are always truthy\n            Self::Type(_) | Self::BuiltinFunction(_) | Self::Function { .. } | Self::Repr(_) | Self::Cycle(_, _) => {\n                true\n            }\n        }\n    }\n\n    /// Returns the Python type name for this value (e.g., `\"int\"`, `\"str\"`, `\"list\"`).\n    ///\n    /// These are the same names returned by Python's `type(x).__name__`.\n    #[must_use]\n    pub fn type_name(&self) -> &'static str {\n        match self {\n            Self::None => \"NoneType\",\n            Self::Ellipsis => \"ellipsis\",\n            Self::Bool(_) => \"bool\",\n            Self::Int(_) | Self::BigInt(_) => \"int\",\n            Self::Float(_) => \"float\",\n            Self::String(_) => \"str\",\n            Self::Bytes(_) => \"bytes\",\n            Self::List(_) => \"list\",\n            Self::Tuple(_) => \"tuple\",\n            Self::NamedTuple { .. } => \"namedtuple\",\n            Self::Dict(_) => \"dict\",\n            Self::Set(_) => \"set\",\n            Self::FrozenSet(_) => \"frozenset\",\n            Self::Exception { .. } => \"Exception\",\n            Self::Path(_) => \"PosixPath\",\n            Self::Dataclass { .. } => \"dataclass\",\n            Self::Type(_) => \"type\",\n            Self::BuiltinFunction(_) => \"builtin_function_or_method\",\n            Self::Function { .. } => \"function\",\n            Self::Repr(_) => \"repr\",\n            Self::Cycle(_, _) => \"cycle\",\n        }\n    }\n}\n\nimpl Hash for MontyObject {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        // Hash the discriminant first (but Int and BigInt share discriminant for consistency)\n        match self {\n            Self::Int(_) | Self::BigInt(_) => {\n                // Use Int discriminant for both to maintain hash consistency\n                std::mem::discriminant(&Self::Int(0)).hash(state);\n            }\n            _ => std::mem::discriminant(self).hash(state),\n        }\n\n        match self {\n            Self::Ellipsis | Self::None => {}\n            Self::Bool(bool) => bool.hash(state),\n            Self::Int(i) => i.hash(state),\n            Self::BigInt(bi) => {\n                // For hash consistency, if BigInt fits in i64, hash as i64\n                if let Ok(i) = i64::try_from(bi) {\n                    i.hash(state);\n                } else {\n                    // For large BigInts, hash the signed bytes\n                    bi.to_signed_bytes_le().hash(state);\n                }\n            }\n            Self::Float(f) => f.to_bits().hash(state),\n            Self::String(string) => string.hash(state),\n            Self::Bytes(bytes) => bytes.hash(state),\n            Self::Path(path) => path.hash(state),\n            Self::Type(t) => t.to_string().hash(state),\n            Self::Cycle(_, _) => panic!(\"cycle values are not hashable\"),\n            _ => panic!(\"{} python values are not hashable\", self.type_name()),\n        }\n    }\n}\n\nimpl PartialEq for MontyObject {\n    fn eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (Self::Ellipsis, Self::Ellipsis) => true,\n            (Self::None, Self::None) => true,\n            (Self::Bool(a), Self::Bool(b)) => a == b,\n            (Self::Int(a), Self::Int(b)) => a == b,\n            (Self::BigInt(a), Self::BigInt(b)) => a == b,\n            // Cross-compare Int and BigInt\n            (Self::Int(a), Self::BigInt(b)) | (Self::BigInt(b), Self::Int(a)) => BigInt::from(*a) == *b,\n            // Use to_bits() for float comparison to be consistent with Hash\n            (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),\n            (Self::String(a), Self::String(b)) => a == b,\n            (Self::Bytes(a), Self::Bytes(b)) => a == b,\n            (Self::List(a), Self::List(b)) => a == b,\n            (Self::Tuple(a), Self::Tuple(b)) => a == b,\n            (\n                Self::NamedTuple {\n                    type_name: a_type,\n                    field_names: a_fields,\n                    values: a_values,\n                },\n                Self::NamedTuple {\n                    type_name: b_type,\n                    field_names: b_fields,\n                    values: b_values,\n                },\n            ) => a_type == b_type && a_fields == b_fields && a_values == b_values,\n            // NamedTuple can compare with Tuple by values only (matching Python semantics)\n            (Self::NamedTuple { values, .. }, Self::Tuple(t)) | (Self::Tuple(t), Self::NamedTuple { values, .. }) => {\n                values == t\n            }\n            (Self::Dict(a), Self::Dict(b)) => a == b,\n            (Self::Set(a), Self::Set(b)) => a == b,\n            (Self::FrozenSet(a), Self::FrozenSet(b)) => a == b,\n            (\n                Self::Exception {\n                    exc_type: a_type,\n                    arg: a_arg,\n                },\n                Self::Exception {\n                    exc_type: b_type,\n                    arg: b_arg,\n                },\n            ) => a_type == b_type && a_arg == b_arg,\n            (\n                Self::Dataclass {\n                    name: a_name,\n                    type_id: a_type_id,\n                    field_names: a_field_names,\n                    attrs: a_attrs,\n                    frozen: a_frozen,\n                },\n                Self::Dataclass {\n                    name: b_name,\n                    type_id: b_type_id,\n                    field_names: b_field_names,\n                    attrs: b_attrs,\n                    frozen: b_frozen,\n                },\n            ) => {\n                a_name == b_name\n                    && a_type_id == b_type_id\n                    && a_field_names == b_field_names\n                    && a_attrs == b_attrs\n                    && a_frozen == b_frozen\n            }\n            (Self::Path(a), Self::Path(b)) => a == b,\n            (\n                Self::Function {\n                    name: a_name,\n                    docstring: a_doc,\n                },\n                Self::Function {\n                    name: b_name,\n                    docstring: b_doc,\n                },\n            ) => a_name == b_name && a_doc == b_doc,\n            (Self::Repr(a), Self::Repr(b)) => a == b,\n            (Self::Cycle(a, _), Self::Cycle(b, _)) => a == b,\n            (Self::Type(a), Self::Type(b)) => a == b,\n            _ => false,\n        }\n    }\n}\n\nimpl Eq for MontyObject {}\n\nimpl AsRef<Self> for MontyObject {\n    fn as_ref(&self) -> &Self {\n        self\n    }\n}\n\n/// Error returned when a `MontyObject` cannot be converted to the requested Rust type.\n///\n/// This error is returned by the `TryFrom` implementations when attempting to extract\n/// a specific type from a `MontyObject` that holds a different variant.\n#[derive(Debug)]\npub struct ConversionError {\n    /// The type name that was expected (e.g., \"int\", \"str\").\n    pub expected: &'static str,\n    /// The actual type name of the `MontyObject` (e.g., \"list\", \"NoneType\").\n    pub actual: &'static str,\n}\n\nimpl ConversionError {\n    /// Creates a new `ConversionError` with the expected and actual type names.\n    #[must_use]\n    pub fn new(expected: &'static str, actual: &'static str) -> Self {\n        Self { expected, actual }\n    }\n}\n\nimpl fmt::Display for ConversionError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"expected {}, got {}\", self.expected, self.actual)\n    }\n}\n\nimpl std::error::Error for ConversionError {}\n\n/// Error returned when a `MontyObject` cannot be used as an input to code execution.\n///\n/// This can occur when:\n/// - A `MontyObject` variant (like `Repr`) is only valid as an output, not an input\n/// - A resource limit (memory, allocations) is exceeded during conversion\n#[derive(Debug, Clone)]\npub enum InvalidInputError {\n    /// The input type is not valid for conversion to a runtime Value.\n    /// Message explaining why the type is invalid.\n    InvalidType(Cow<'static, str>),\n    /// A resource limit was exceeded during conversion.\n    Resource(ResourceError),\n}\n\nimpl InvalidInputError {\n    /// Creates a new `InvalidInputError` for the given type name.\n    #[must_use]\n    pub fn invalid_type(msg: impl Into<Cow<'static, str>>) -> Self {\n        Self::InvalidType(msg.into())\n    }\n}\n\nimpl fmt::Display for InvalidInputError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::InvalidType(msg) => write!(f, \"{msg}\"),\n            Self::Resource(e) => write!(f, \"{e}\"),\n        }\n    }\n}\n\nimpl std::error::Error for InvalidInputError {}\n\nimpl From<crate::resource::ResourceError> for InvalidInputError {\n    fn from(err: crate::resource::ResourceError) -> Self {\n        Self::Resource(err)\n    }\n}\n\n/// Attempts to convert a MontyObject to an i64 integer.\n/// Returns an error if the object is not an Int variant.\nimpl TryFrom<&MontyObject> for i64 {\n    type Error = ConversionError;\n\n    fn try_from(value: &MontyObject) -> Result<Self, Self::Error> {\n        match value {\n            MontyObject::Int(i) => Ok(*i),\n            _ => Err(ConversionError::new(\"int\", value.type_name())),\n        }\n    }\n}\n\n/// Attempts to convert a MontyObject to an f64 float.\n/// Returns an error if the object is not a Float or Int variant.\n/// Int values are automatically converted to f64 to match python's behavior.\nimpl TryFrom<&MontyObject> for f64 {\n    type Error = ConversionError;\n\n    fn try_from(value: &MontyObject) -> Result<Self, Self::Error> {\n        match value {\n            MontyObject::Float(f) => Ok(*f),\n            MontyObject::Int(i) => Ok(*i as Self),\n            _ => Err(ConversionError::new(\"float\", value.type_name())),\n        }\n    }\n}\n\n/// Attempts to convert a MontyObject to a String.\n/// Returns an error if the object is not a heap-allocated Str variant.\nimpl TryFrom<&MontyObject> for String {\n    type Error = ConversionError;\n\n    fn try_from(value: &MontyObject) -> Result<Self, Self::Error> {\n        if let MontyObject::String(s) = value {\n            Ok(s.clone())\n        } else {\n            Err(ConversionError::new(\"str\", value.type_name()))\n        }\n    }\n}\n\n/// Attempts to convert a `MontyObject` to a bool.\n/// Returns an error if the object is not a True or False variant.\n/// Note: This does NOT use Python's truthiness rules (use MontyObject::bool for that).\nimpl TryFrom<&MontyObject> for bool {\n    type Error = ConversionError;\n\n    fn try_from(value: &MontyObject) -> Result<Self, Self::Error> {\n        match value {\n            MontyObject::Bool(b) => Ok(*b),\n            _ => Err(ConversionError::new(\"bool\", value.type_name())),\n        }\n    }\n}\n\n/// A collection of key-value pairs representing Python dictionary contents.\n///\n/// Used internally by `MontyObject::Dict` to store dictionary entries while preserving\n/// insertion order. Keys and values are both `MontyObject` instances.\n#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct DictPairs(Vec<(MontyObject, MontyObject)>);\n\nimpl From<Vec<(MontyObject, MontyObject)>> for DictPairs {\n    fn from(pairs: Vec<(MontyObject, MontyObject)>) -> Self {\n        Self(pairs)\n    }\n}\n\nimpl From<IndexMap<MontyObject, MontyObject>> for DictPairs {\n    fn from(map: IndexMap<MontyObject, MontyObject>) -> Self {\n        Self(map.into_iter().collect())\n    }\n}\n\nimpl From<DictPairs> for IndexMap<MontyObject, MontyObject> {\n    fn from(pairs: DictPairs) -> Self {\n        pairs.into_iter().collect()\n    }\n}\n\nimpl IntoIterator for DictPairs {\n    type Item = (MontyObject, MontyObject);\n    type IntoIter = std::vec::IntoIter<Self::Item>;\n\n    fn into_iter(self) -> Self::IntoIter {\n        self.0.into_iter()\n    }\n}\nimpl<'a> IntoIterator for &'a DictPairs {\n    type Item = &'a (MontyObject, MontyObject);\n    type IntoIter = std::slice::Iter<'a, (MontyObject, MontyObject)>;\n\n    fn into_iter(self) -> Self::IntoIter {\n        self.0.iter()\n    }\n}\n\nimpl FromIterator<(MontyObject, MontyObject)> for DictPairs {\n    fn from_iter<T: IntoIterator<Item = (MontyObject, MontyObject)>>(iter: T) -> Self {\n        Self(iter.into_iter().collect())\n    }\n}\n\nimpl DictPairs {\n    fn is_empty(&self) -> bool {\n        self.0.is_empty()\n    }\n\n    fn iter(&self) -> impl Iterator<Item = &(MontyObject, MontyObject)> {\n        self.0.iter()\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/os.rs",
    "content": "//! OS-level operations that require host system access.\n//!\n//! This module defines the `OsFunction` enum, which represents operations that\n//! cannot be performed in a sandboxed environment. When a type method needs to\n//! perform one of these operations, it returns an `CallResult::OsCall` variant\n//! with the function and arguments. The VM then yields control to the host via\n//! `FrameExit::OsCall`, allowing the host to execute the operation and resume.\n//!\n//! This design enables sandboxed execution: the interpreter never directly performs\n//! I/O, filesystem, or network operations. Instead, the host decides whether to\n//! permit and execute such operations.\n\nuse crate::{MontyObject, intern::StaticStrings};\n\n/// OS operations that require host system access.\n///\n/// These represent operations that Monty cannot perform in isolation because\n/// they require interacting with the operating system (filesystem, network, etc.).\n/// The host application decides whether to permit and execute these operations.\n///\n/// # Extension\n///\n/// When adding new operations, add both the variant here and update the\n/// `TryFrom<StaticStrings>` implementation to map method names to operations.\n// #[repr(u8)]\n#[derive(\n    Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString, strum::Display, serde::Serialize, serde::Deserialize,\n)]\npub enum OsFunction {\n    /// Check if a path exists\n    #[strum(serialize = \"Path.exists\")]\n    Exists,\n    /// Check if path is a file\n    #[strum(serialize = \"Path.is_file\")]\n    IsFile,\n    /// Check if path is a directory\n    #[strum(serialize = \"Path.is_dir\")]\n    IsDir,\n    /// Check if path is a symbolic link\n    #[strum(serialize = \"Path.is_symlink\")]\n    IsSymlink,\n    /// Read file contents as text\n    #[strum(serialize = \"Path.read_text\")]\n    ReadText,\n    /// Read file contents as bytes\n    #[strum(serialize = \"Path.read_bytes\")]\n    ReadBytes,\n    /// Write text to file\n    #[strum(serialize = \"Path.write_text\")]\n    WriteText,\n    /// Write bytes to file\n    #[strum(serialize = \"Path.write_bytes\")]\n    WriteBytes,\n    /// Create directory\n    #[strum(serialize = \"Path.mkdir\")]\n    Mkdir,\n    /// Remove file\n    #[strum(serialize = \"Path.unlink\")]\n    Unlink,\n    /// Remove directory\n    #[strum(serialize = \"Path.rmdir\")]\n    Rmdir,\n    /// List directory contents\n    #[strum(serialize = \"Path.iterdir\")]\n    Iterdir,\n    /// Get file stats\n    #[strum(serialize = \"Path.stat\")]\n    Stat,\n    /// Rename/move file\n    #[strum(serialize = \"Path.rename\")]\n    Rename,\n    /// Get resolved absolute path\n    #[strum(serialize = \"Path.resolve\")]\n    Resolve,\n    /// Get absolute path (without resolving symlinks)\n    #[strum(serialize = \"Path.absolute\")]\n    Absolute,\n    /// Get an environment variable value\n    #[strum(serialize = \"os.getenv\")]\n    Getenv,\n    /// Get the entire environment as a dictionary\n    #[strum(serialize = \"os.environ\")]\n    GetEnviron,\n}\n\nimpl TryFrom<StaticStrings> for OsFunction {\n    type Error = ();\n\n    /// Attempts to convert a method name (as a `StaticStrings` variant) to an `OsFunction`.\n    ///\n    /// Returns `Err(())` if the method name doesn't correspond to an OS operation.\n    fn try_from(method: StaticStrings) -> Result<Self, Self::Error> {\n        match method {\n            // Read operations\n            StaticStrings::Exists => Ok(Self::Exists),\n            StaticStrings::IsFile => Ok(Self::IsFile),\n            StaticStrings::IsDir => Ok(Self::IsDir),\n            StaticStrings::IsSymlink => Ok(Self::IsSymlink),\n            StaticStrings::ReadText => Ok(Self::ReadText),\n            StaticStrings::ReadBytes => Ok(Self::ReadBytes),\n            StaticStrings::StatMethod => Ok(Self::Stat),\n            StaticStrings::Iterdir => Ok(Self::Iterdir),\n            StaticStrings::Resolve => Ok(Self::Resolve),\n            StaticStrings::Absolute => Ok(Self::Absolute),\n            // Write operations\n            StaticStrings::WriteText => Ok(Self::WriteText),\n            StaticStrings::WriteBytes => Ok(Self::WriteBytes),\n            StaticStrings::Mkdir => Ok(Self::Mkdir),\n            StaticStrings::Unlink => Ok(Self::Unlink),\n            StaticStrings::Rmdir => Ok(Self::Rmdir),\n            StaticStrings::Rename => Ok(Self::Rename),\n            _ => Err(()),\n        }\n    }\n}\n\n// =============================================================================\n// stat_result builders\n// =============================================================================\n// These functions create MontyObject::NamedTuple values that match Python's\n// os.stat_result structure. The stat_result has 10 fields:\n// st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime\n\nconst STAT_RESULT_TYPE_NAME: &str = \"StatResult\";\nconst STAT_RESULT_FIELDS: &[&str] = &[\n    \"st_mode\", \"st_ino\", \"st_dev\", \"st_nlink\", \"st_uid\", \"st_gid\", \"st_size\", \"st_atime\", \"st_mtime\", \"st_ctime\",\n];\n\n/// Creates a stat_result for a regular file.\n///\n/// The file type bits (`0o100_000`) are automatically added if not present.\n///\n/// # Arguments\n/// * `mode` - File permissions as octal. Common values:\n///   - `0o644` - rw-r--r-- (owner read/write, others read)\n///   - `0o600` - rw------- (owner read/write only)\n///   - `0o755` - rwxr-xr-x (executable, owner full, others read/execute)\n///   - `0o100644` - same as 0o644 with explicit file type bits\n/// * `size` - File size in bytes\n/// * `mtime` - Modification time as Unix timestamp\n#[must_use]\npub fn file_stat(mode: i64, size: i64, mtime: f64) -> MontyObject {\n    // If only permission bits provided (no file type), add regular file type\n    let mode = if mode < 0o1000 { mode | 0o100_000 } else { mode };\n    stat_result(mode, 0, 0, 1, 0, 0, size, mtime, mtime, mtime)\n}\n\n/// Creates a stat_result for a directory.\n///\n/// The directory type bits (`0o040_000`) are automatically added if not present.\n///\n/// # Arguments\n/// * `mode` - Directory permissions as octal. Common values:\n///   - `0o755` - rwxr-xr-x (owner full, others read/execute)\n///   - `0o700` - rwx------ (owner only)\n///   - `0o040755` - same as 0o755 with explicit directory type bits\n/// * `mtime` - Modification time as Unix timestamp\n#[must_use]\npub fn dir_stat(mode: i64, mtime: f64) -> MontyObject {\n    // If only permission bits provided (no file type), add directory type\n    let mode = if mode < 0o1000 { mode | 0o040_000 } else { mode };\n    stat_result(mode, 0, 0, 2, 0, 0, 4096, mtime, mtime, mtime)\n}\n\n/// Creates a stat_result for a symbolic link.\n///\n/// The symlink type bits (`0o120_000`) are automatically added if not present.\n///\n/// # Arguments\n/// * `mode` - Symlink permissions as octal. Common values:\n///   - `0o777` - rwxrwxrwx (symlinks typically have full permissions)\n///   - `0o120777` - same as 0o777 with explicit symlink type bits\n/// * `mtime` - Modification time as Unix timestamp\n#[must_use]\npub fn symlink_stat(mode: i64, mtime: f64) -> MontyObject {\n    // If only permission bits provided (no file type), add symlink type\n    let mode = if mode < 0o1000 { mode | 0o120_000 } else { mode };\n    stat_result(mode, 0, 0, 1, 0, 0, 0, mtime, mtime, mtime)\n}\n\n/// Creates a full stat_result with all 10 fields specified.\n///\n/// This is the low-level builder; prefer `file_stat()`, `dir_stat()`, or `symlink_stat()`\n/// for common cases.\n#[must_use]\n#[expect(clippy::too_many_arguments)]\npub fn stat_result(\n    st_mode: i64,\n    st_ino: i64,\n    st_dev: i64,\n    st_nlink: i64,\n    st_uid: i64,\n    st_gid: i64,\n    st_size: i64,\n    st_atime: f64,\n    st_mtime: f64,\n    st_ctime: f64,\n) -> MontyObject {\n    MontyObject::NamedTuple {\n        type_name: STAT_RESULT_TYPE_NAME.to_owned(),\n        field_names: STAT_RESULT_FIELDS.iter().map(|s| (*s).to_owned()).collect(),\n        values: vec![\n            MontyObject::Int(st_mode),\n            MontyObject::Int(st_ino),\n            MontyObject::Int(st_dev),\n            MontyObject::Int(st_nlink),\n            MontyObject::Int(st_uid),\n            MontyObject::Int(st_gid),\n            MontyObject::Int(st_size),\n            MontyObject::Float(st_atime),\n            MontyObject::Float(st_mtime),\n            MontyObject::Float(st_ctime),\n        ],\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/parse.rs",
    "content": "use std::{borrow::Cow, fmt};\n\nuse num_bigint::BigInt;\nuse ruff_python_ast::{\n    self as ast, BoolOp, CmpOp, ConversionFlag as RuffConversionFlag, ElifElseClause, Expr as AstExpr,\n    InterpolatedStringElement, Keyword, Number, Operator as AstOperator, ParameterWithDefault, Stmt, UnaryOp,\n    name::Name,\n};\nuse ruff_python_parser::parse_module;\nuse ruff_text_size::{Ranged, TextRange};\n\nuse crate::{\n    StackFrame,\n    args::{ArgExprs, CallArg, CallKwarg, Kwarg},\n    exception_private::ExcType,\n    exception_public::{CodeLoc, MontyException},\n    expressions::{\n        Callable, CmpOperator, Comprehension, DictItem, Expr, ExprLoc, Identifier, Literal, Node, Operator,\n        SequenceItem, UnpackTarget,\n    },\n    fstring::{ConversionFlag, FStringPart, FormatSpec},\n    intern::{InternerBuilder, StringId},\n    value::EitherStr,\n};\n\n/// Maximum nesting depth for AST structures during parsing.\n/// Matches CPython's limit of ~200 for nested parentheses.\n/// This prevents stack overflow from deeply nested structures like `((((x,),),),)`.\n#[cfg(not(debug_assertions))]\npub const MAX_NESTING_DEPTH: u16 = 200;\n/// In debug builds, we use a lower limit because stack frames are much larger\n/// (no inlining, debug info, etc.). The limit is set conservatively to prevent\n/// stack overflow while still catching the error before the recursion limit.\n#[cfg(debug_assertions)]\npub const MAX_NESTING_DEPTH: u16 = 35;\n\n/// A parameter in a function signature with optional default value.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct ParsedParam {\n    /// The parameter name.\n    pub name: StringId,\n    /// The default value expression (evaluated at definition time).\n    pub default: Option<ExprLoc>,\n}\n\n/// A parsed function signature with all parameter types.\n///\n/// This intermediate representation captures the structure of Python function\n/// parameters before name resolution. Default value expressions are stored\n/// as unevaluated AST and will be evaluated during the prepare phase.\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct ParsedSignature {\n    /// Positional-only parameters (before `/`).\n    pub pos_args: Vec<ParsedParam>,\n    /// Positional-or-keyword parameters.\n    pub args: Vec<ParsedParam>,\n    /// Variable positional parameter (`*args`).\n    pub var_args: Option<StringId>,\n    /// Keyword-only parameters (after `*` or `*args`).\n    pub kwargs: Vec<ParsedParam>,\n    /// Variable keyword parameter (`**kwargs`).\n    pub var_kwargs: Option<StringId>,\n}\n\nimpl ParsedSignature {\n    /// Returns an iterator over all parameter names in the signature.\n    ///\n    /// Order: pos_args, args, var_args, kwargs, var_kwargs\n    pub fn param_names(&self) -> impl Iterator<Item = StringId> + '_ {\n        self.pos_args\n            .iter()\n            .map(|p| p.name)\n            .chain(self.args.iter().map(|p| p.name))\n            .chain(self.var_args.iter().copied())\n            .chain(self.kwargs.iter().map(|p| p.name))\n            .chain(self.var_kwargs.iter().copied())\n    }\n}\n\n/// A raw (unprepared) function definition from the parser.\n///\n/// Contains the function name, signature, and body as parsed AST nodes.\n/// During the prepare phase, this is transformed into `PreparedFunctionDef`\n/// with resolved names and scope information.\n#[derive(Debug, Clone)]\npub struct RawFunctionDef {\n    /// The function name identifier (not yet resolved to a namespace index).\n    pub name: Identifier,\n    /// The parsed function signature with parameter names and default expressions.\n    pub signature: ParsedSignature,\n    /// The unprepared function body (names not yet resolved).\n    pub body: Vec<ParseNode>,\n    /// Whether this is an async function (`async def`).\n    pub is_async: bool,\n}\n\n/// Type alias for parsed AST nodes (output of the parser).\n///\n/// This uses `Node<RawFunctionDef>` where function definitions contain their\n/// full unprepared body. After the prepare phase, this becomes `PreparedNode`\n/// (aka `Node<PreparedFunctionDef>`).\npub type ParseNode = Node<RawFunctionDef>;\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct Try<N> {\n    pub body: Vec<N>,\n    pub handlers: Vec<ExceptHandler<N>>,\n    pub or_else: Vec<N>,\n    pub finally: Vec<N>,\n}\n\n/// A parsed exception handler (except clause).\n///\n/// Represents `except ExcType as name:` or bare `except:` clauses.\n/// The exception type and variable binding are both optional.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct ExceptHandler<N> {\n    /// Exception type(s) to catch. None = bare except (catches all).\n    pub exc_type: Option<ExprLoc>,\n    /// Variable name for `except X as e:`. None = no binding.\n    pub name: Option<Identifier>,\n    /// Handler body statements.\n    pub body: Vec<N>,\n}\n\n/// Result of parsing: the AST nodes and the string interner with all interned names.\n#[derive(Debug)]\npub struct ParseResult {\n    pub nodes: Vec<ParseNode>,\n    pub interner: InternerBuilder,\n}\n\npub(crate) fn parse(code: &str, filename: &str) -> Result<ParseResult, ParseError> {\n    parse_with_interner(code, filename, InternerBuilder::new(code))\n}\n\n/// Parses code using a caller-provided interner seed.\n///\n/// This enables incremental compilation flows (e.g. REPL) where existing\n/// interned IDs must remain stable across parse invocations.\npub(crate) fn parse_with_interner(\n    code: &str,\n    filename: &str,\n    interner: InternerBuilder,\n) -> Result<ParseResult, ParseError> {\n    let mut parser = Parser::new(code, filename, interner);\n    let parsed = parse_module(code).map_err(|e| ParseError::syntax(e.to_string(), parser.convert_range(e.range())))?;\n    let module = parsed.into_syntax();\n    let nodes = parser.parse_statements(module.body)?;\n    Ok(ParseResult {\n        nodes,\n        interner: parser.interner,\n    })\n}\n\n/// Parser for converting ruff AST to Monty's intermediate ParseNode representation.\n///\n/// Holds references to the source code and owns a string interner for names.\n/// The filename is interned once at construction and reused for all CodeRanges.\npub struct Parser<'a> {\n    line_ends: Vec<usize>,\n    code: &'a str,\n    /// Interned filename ID, used for all CodeRanges created by this parser.\n    filename_id: StringId,\n    /// String interner for names (variables, functions, etc).\n    pub interner: InternerBuilder,\n    /// Remaining nesting depth budget for recursive structures.\n    /// Starts at MAX_NESTING_DEPTH and decrements on each nested level.\n    /// When it reaches zero, we return a \"too many nested parentheses\" error.\n    depth_remaining: u16,\n}\n\nimpl<'a> Parser<'a> {\n    fn new(code: &'a str, filename: &'a str, mut interner: InternerBuilder) -> Self {\n        // Position of each line in the source code, to convert indexes to line number and column number\n        let mut line_ends = vec![];\n        for (i, c) in code.chars().enumerate() {\n            if c == '\\n' {\n                line_ends.push(i);\n            }\n        }\n        let filename_id = interner.intern(filename);\n        Self {\n            line_ends,\n            code,\n            filename_id,\n            interner,\n            depth_remaining: MAX_NESTING_DEPTH,\n        }\n    }\n\n    fn parse_statements(&mut self, statements: Vec<Stmt>) -> Result<Vec<ParseNode>, ParseError> {\n        statements.into_iter().map(|f| self.parse_statement(f)).collect()\n    }\n\n    fn parse_elif_else_clauses(&mut self, clauses: Vec<ElifElseClause>) -> Result<Vec<ParseNode>, ParseError> {\n        let mut tail: Vec<ParseNode> = Vec::new();\n        for clause in clauses.into_iter().rev() {\n            match clause.test {\n                Some(test) => {\n                    let test = self.parse_expression(test)?;\n                    let body = self.parse_statements(clause.body)?;\n                    let or_else = tail;\n                    let nested = Node::If { test, body, or_else };\n                    tail = vec![nested];\n                }\n                None => {\n                    tail = self.parse_statements(clause.body)?;\n                }\n            }\n        }\n        Ok(tail)\n    }\n\n    /// Parses an exception handler (except clause).\n    ///\n    /// Handles `except:`, `except ExcType:`, and `except ExcType as name:` forms.\n    fn parse_except_handler(\n        &mut self,\n        handler: ruff_python_ast::ExceptHandler,\n    ) -> Result<ExceptHandler<ParseNode>, ParseError> {\n        let ruff_python_ast::ExceptHandler::ExceptHandler(h) = handler;\n        let exc_type = match h.type_ {\n            Some(expr) => Some(self.parse_expression(*expr)?),\n            None => None,\n        };\n        let name = h.name.map(|n| self.identifier(&n.id, n.range));\n        let body = self.parse_statements(h.body)?;\n        Ok(ExceptHandler { exc_type, name, body })\n    }\n\n    fn parse_statement(&mut self, statement: Stmt) -> Result<ParseNode, ParseError> {\n        self.decr_depth_remaining(|| statement.range())?;\n        let result = self.parse_statement_impl(statement);\n        self.depth_remaining += 1;\n        result\n    }\n\n    fn parse_statement_impl(&mut self, statement: Stmt) -> Result<ParseNode, ParseError> {\n        match statement {\n            Stmt::FunctionDef(function) => {\n                let params = &function.parameters;\n\n                // Parse positional-only parameters (before /)\n                let pos_args = self.parse_params_with_defaults(&params.posonlyargs)?;\n\n                // Parse positional-or-keyword parameters\n                let args = self.parse_params_with_defaults(&params.args)?;\n\n                // Parse *args\n                let var_args = params.vararg.as_ref().map(|p| self.interner.intern(&p.name.id));\n\n                // Parse keyword-only parameters (after * or *args)\n                let kwargs = self.parse_params_with_defaults(&params.kwonlyargs)?;\n\n                // Parse **kwargs\n                let var_kwargs = params.kwarg.as_ref().map(|p| self.interner.intern(&p.name.id));\n\n                let signature = ParsedSignature {\n                    pos_args,\n                    args,\n                    var_args,\n                    kwargs,\n                    var_kwargs,\n                };\n\n                let name = self.identifier(&function.name.id, function.name.range);\n                // Parse function body recursively\n                let body = self.parse_statements(function.body)?;\n                let is_async = function.is_async;\n\n                Ok(Node::FunctionDef(RawFunctionDef {\n                    name,\n                    signature,\n                    body,\n                    is_async,\n                }))\n            }\n            Stmt::ClassDef(c) => Err(ParseError::not_implemented(\n                \"class definitions\",\n                self.convert_range(c.range),\n            )),\n            Stmt::Return(ast::StmtReturn { value, .. }) => match value {\n                Some(value) => Ok(Node::Return(self.parse_expression(*value)?)),\n                None => Ok(Node::ReturnNone),\n            },\n            Stmt::Delete(d) => Err(ParseError::not_implemented(\n                \"the 'del' statement\",\n                self.convert_range(d.range),\n            )),\n            Stmt::TypeAlias(t) => Err(ParseError::not_implemented(\"type aliases\", self.convert_range(t.range))),\n            Stmt::Assign(ast::StmtAssign {\n                targets, value, range, ..\n            }) => self.parse_assignment(first(targets, self.convert_range(range))?, *value),\n            Stmt::AugAssign(ast::StmtAugAssign { target, op, value, .. }) => {\n                let op = convert_op(op);\n                let value = self.parse_expression(*value)?;\n                match *target {\n                    AstExpr::Subscript(ast::ExprSubscript {\n                        value: object,\n                        slice,\n                        range,\n                        ..\n                    }) => Ok(Node::SubscriptOpAssign {\n                        target: self.parse_identifier(*object)?,\n                        index: self.parse_expression(*slice)?,\n                        op,\n                        object: value,\n                        target_position: self.convert_range(range),\n                    }),\n                    other => Ok(Node::OpAssign {\n                        target: self.parse_identifier(other)?,\n                        op,\n                        object: value,\n                    }),\n                }\n            }\n            Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => match value {\n                Some(value) => self.parse_assignment(*target, *value),\n                None => Ok(Node::Pass),\n            },\n            Stmt::For(ast::StmtFor {\n                is_async,\n                target,\n                iter,\n                body,\n                orelse,\n                range,\n                ..\n            }) => {\n                if is_async {\n                    return Err(ParseError::not_implemented(\n                        \"async for loops\",\n                        self.convert_range(range),\n                    ));\n                }\n                Ok(Node::For {\n                    target: self.parse_unpack_target(*target)?,\n                    iter: self.parse_expression(*iter)?,\n                    body: self.parse_statements(body)?,\n                    or_else: self.parse_statements(orelse)?,\n                })\n            }\n            Stmt::While(ast::StmtWhile { test, body, orelse, .. }) => Ok(Node::While {\n                test: self.parse_expression(*test)?,\n                body: self.parse_statements(body)?,\n                or_else: self.parse_statements(orelse)?,\n            }),\n            Stmt::If(ast::StmtIf {\n                test,\n                body,\n                elif_else_clauses,\n                ..\n            }) => {\n                let test = self.parse_expression(*test)?;\n                let body = self.parse_statements(body)?;\n                let or_else = self.parse_elif_else_clauses(elif_else_clauses)?;\n                Ok(Node::If { test, body, or_else })\n            }\n            Stmt::With(ast::StmtWith { is_async, range, .. }) => {\n                if is_async {\n                    Err(ParseError::not_implemented(\n                        \"async context managers (async with)\",\n                        self.convert_range(range),\n                    ))\n                } else {\n                    Err(ParseError::not_implemented(\n                        \"context managers (with statements)\",\n                        self.convert_range(range),\n                    ))\n                }\n            }\n            Stmt::Match(m) => Err(ParseError::not_implemented(\n                \"pattern matching (match statements)\",\n                self.convert_range(m.range),\n            )),\n            Stmt::Raise(ast::StmtRaise { exc, .. }) => {\n                // TODO add cause to Node::Raise\n                let expr = match exc {\n                    Some(expr) => Some(self.parse_expression(*expr)?),\n                    None => None,\n                };\n                Ok(Node::Raise(expr))\n            }\n            Stmt::Try(ast::StmtTry {\n                body,\n                handlers,\n                orelse,\n                finalbody,\n                is_star,\n                range,\n                ..\n            }) => {\n                if is_star {\n                    Err(ParseError::not_implemented(\n                        \"exception groups (try*/except*)\",\n                        self.convert_range(range),\n                    ))\n                } else {\n                    let body = self.parse_statements(body)?;\n                    let handlers = handlers\n                        .into_iter()\n                        .map(|h| self.parse_except_handler(h))\n                        .collect::<Result<Vec<_>, _>>()?;\n                    let or_else = self.parse_statements(orelse)?;\n                    let finally = self.parse_statements(finalbody)?;\n                    Ok(Node::Try(Try {\n                        body,\n                        handlers,\n                        or_else,\n                        finally,\n                    }))\n                }\n            }\n            Stmt::Assert(ast::StmtAssert { test, msg, .. }) => {\n                let test = self.parse_expression(*test)?;\n                let msg = match msg {\n                    Some(m) => Some(self.parse_expression(*m)?),\n                    None => None,\n                };\n                Ok(Node::Assert { test, msg })\n            }\n            Stmt::Import(ast::StmtImport { names, range, .. }) => {\n                // We only support single module imports (e.g., `import sys`)\n                // Multi-module imports (e.g., `import sys, os`) are not supported\n                let position = self.convert_range(range);\n                if names.len() != 1 {\n                    return Err(ParseError::not_implemented(\"multi-module import statements\", position));\n                }\n                let alias_node = &names[0];\n                let module_name = self.interner.intern(&alias_node.name);\n                // The binding name is the alias if present, otherwise the module name\n                let binding_name = alias_node\n                    .asname\n                    .as_ref()\n                    .map_or(module_name, |n| self.interner.intern(&n.id));\n                // Create an unresolved identifier (namespace slot will be set during prepare)\n                let binding = Identifier::new(binding_name, position);\n                Ok(Node::Import { module_name, binding })\n            }\n            Stmt::ImportFrom(ast::StmtImportFrom {\n                module,\n                names,\n                level,\n                range,\n                ..\n            }) => {\n                let position = self.convert_range(range);\n                // We only support absolute imports (level 0)\n                if level != 0 {\n                    return Err(ParseError::import_error(\n                        \"attempted relative import with no known parent package\",\n                        position,\n                    ));\n                }\n                // Module name is required for absolute imports\n                let module_name = match module {\n                    Some(m) => self.interner.intern(&m),\n                    None => {\n                        return Err(ParseError::import_error(\n                            \"attempted relative import with no known parent package\",\n                            position,\n                        ));\n                    }\n                };\n                // Parse the imported names\n                let names = names\n                    .iter()\n                    .map(|alias| {\n                        // Check for star import which is not supported\n                        if alias.name.as_str() == \"*\" {\n                            return Err(ParseError::not_supported(\n                                \"Wildcard imports (`from ... import *`) are not supported\",\n                                position,\n                            ));\n                        }\n                        let name = self.interner.intern(&alias.name);\n                        // The binding name is the alias if provided, otherwise the import name\n                        let binding_name = alias.asname.as_ref().map_or(name, |n| self.interner.intern(&n.id));\n                        // Create an unresolved identifier (namespace slot will be set during prepare)\n                        let binding = Identifier::new(binding_name, position);\n                        Ok((name, binding))\n                    })\n                    .collect::<Result<Vec<_>, _>>()?;\n                Ok(Node::ImportFrom {\n                    module_name,\n                    names,\n                    position,\n                })\n            }\n            Stmt::Global(ast::StmtGlobal { names, range, .. }) => {\n                let names = names\n                    .iter()\n                    .map(|id| self.interner.intern(&self.code[id.range]))\n                    .collect();\n                Ok(Node::Global {\n                    position: self.convert_range(range),\n                    names,\n                })\n            }\n            Stmt::Nonlocal(ast::StmtNonlocal { names, range, .. }) => {\n                let names = names\n                    .iter()\n                    .map(|id| self.interner.intern(&self.code[id.range]))\n                    .collect();\n                Ok(Node::Nonlocal {\n                    position: self.convert_range(range),\n                    names,\n                })\n            }\n            Stmt::Expr(ast::StmtExpr { value, .. }) => self.parse_expression(*value).map(Node::Expr),\n            Stmt::Pass(_) => Ok(Node::Pass),\n            Stmt::Break(b) => Ok(Node::Break {\n                position: self.convert_range(b.range),\n            }),\n            Stmt::Continue(c) => Ok(Node::Continue {\n                position: self.convert_range(c.range),\n            }),\n            Stmt::IpyEscapeCommand(i) => Err(ParseError::not_implemented(\n                \"IPython escape commands\",\n                self.convert_range(i.range),\n            )),\n        }\n    }\n\n    /// `lhs = rhs` -> `lhs, rhs`\n    /// Handles simple assignments (x = value), subscript assignments (dict[key] = value),\n    /// attribute assignments (obj.attr = value), and tuple unpacking (a, b = value)\n    fn parse_assignment(&mut self, lhs: AstExpr, rhs: AstExpr) -> Result<ParseNode, ParseError> {\n        match lhs {\n            // Subscript assignment like dict[key] = value\n            AstExpr::Subscript(ast::ExprSubscript {\n                value, slice, range, ..\n            }) => Ok(Node::SubscriptAssign {\n                target: self.parse_identifier(*value)?,\n                index: self.parse_expression(*slice)?,\n                value: self.parse_expression(rhs)?,\n                target_position: self.convert_range(range),\n            }),\n            // Attribute assignment like obj.attr = value (supports chained like a.b.c = value)\n            AstExpr::Attribute(ast::ExprAttribute { value, attr, range, .. }) => Ok(Node::AttrAssign {\n                object: self.parse_expression(*value)?,\n                attr: EitherStr::Interned(self.interner.intern(attr.id())),\n                target_position: self.convert_range(range),\n                value: self.parse_expression(rhs)?,\n            }),\n            // Tuple unpacking like a, b = value or (a, b), c = nested\n            AstExpr::Tuple(ast::ExprTuple { elts, range, .. }) => {\n                let targets_position = self.convert_range(range);\n                let targets = elts\n                    .into_iter()\n                    .map(|e| self.parse_unpack_target(e)) // Use parse_unpack_target for recursion\n                    .collect::<Result<Vec<_>, _>>()?;\n                Ok(Node::UnpackAssign {\n                    targets,\n                    targets_position,\n                    object: self.parse_expression(rhs)?,\n                })\n            }\n            // List unpacking like [a, b] = value or [a, *rest] = value\n            AstExpr::List(ast::ExprList { elts, range, .. }) => {\n                let targets_position = self.convert_range(range);\n                let targets = elts\n                    .into_iter()\n                    .map(|e| self.parse_unpack_target(e))\n                    .collect::<Result<Vec<_>, _>>()?;\n                Ok(Node::UnpackAssign {\n                    targets,\n                    targets_position,\n                    object: self.parse_expression(rhs)?,\n                })\n            }\n            // Simple identifier assignment like x = value\n            _ => Ok(Node::Assign {\n                target: self.parse_identifier(lhs)?,\n                object: self.parse_expression(rhs)?,\n            }),\n        }\n    }\n\n    /// Parses an expression from the ruff AST into Monty's ExprLoc representation.\n    ///\n    /// Includes depth tracking to prevent stack overflow from deeply nested structures.\n    /// Matches CPython's limit of 200 for nested parentheses.\n    fn parse_expression(&mut self, expression: AstExpr) -> Result<ExprLoc, ParseError> {\n        self.decr_depth_remaining(|| expression.range())?;\n        let result = self.parse_expression_impl(expression);\n        self.depth_remaining += 1;\n        result\n    }\n\n    fn parse_expression_impl(&mut self, expression: AstExpr) -> Result<ExprLoc, ParseError> {\n        match expression {\n            AstExpr::BoolOp(ast::ExprBoolOp { op, values, range, .. }) => {\n                // Handle chained boolean operations like `a and b and c` by right-folding\n                // into nested binary operations: `a and (b and c)`\n                let rust_op = convert_bool_op(op);\n                let position = self.convert_range(range);\n                let mut values_iter = values.into_iter().rev();\n\n                // Start with the rightmost value\n                let last_value = values_iter.next().expect(\"Expected at least one value in boolean op\");\n                let mut result = self.parse_expression(last_value)?;\n\n                // Fold from right to left\n                for value in values_iter {\n                    let left = Box::new(self.parse_expression(value)?);\n                    result = ExprLoc::new(\n                        position,\n                        Expr::Op {\n                            left,\n                            op: rust_op.clone(),\n                            right: Box::new(result),\n                        },\n                    );\n                }\n                Ok(result)\n            }\n            AstExpr::Named(ast::ExprNamed {\n                target, value, range, ..\n            }) => {\n                let target_ident = self.parse_identifier(*target)?;\n                let value_expr = self.parse_expression(*value)?;\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::Named {\n                        target: target_ident,\n                        value: Box::new(value_expr),\n                    },\n                ))\n            }\n            AstExpr::BinOp(ast::ExprBinOp {\n                left, op, right, range, ..\n            }) => {\n                let left = Box::new(self.parse_expression(*left)?);\n                let right = Box::new(self.parse_expression(*right)?);\n                Ok(ExprLoc {\n                    position: self.convert_range(range),\n                    expr: Expr::Op {\n                        left,\n                        op: convert_op(op),\n                        right,\n                    },\n                })\n            }\n            AstExpr::UnaryOp(ast::ExprUnaryOp { op, operand, range, .. }) => match op {\n                UnaryOp::Not => {\n                    let operand = Box::new(self.parse_expression(*operand)?);\n                    Ok(ExprLoc::new(self.convert_range(range), Expr::Not(operand)))\n                }\n                UnaryOp::USub => {\n                    let operand = Box::new(self.parse_expression(*operand)?);\n                    Ok(ExprLoc::new(self.convert_range(range), Expr::UnaryMinus(operand)))\n                }\n                UnaryOp::UAdd => {\n                    let operand = Box::new(self.parse_expression(*operand)?);\n                    Ok(ExprLoc::new(self.convert_range(range), Expr::UnaryPlus(operand)))\n                }\n                UnaryOp::Invert => {\n                    let operand = Box::new(self.parse_expression(*operand)?);\n                    Ok(ExprLoc::new(self.convert_range(range), Expr::UnaryInvert(operand)))\n                }\n            },\n            AstExpr::Lambda(ast::ExprLambda {\n                parameters,\n                body,\n                range,\n                ..\n            }) => {\n                let position = self.convert_range(range);\n\n                // Intern the lambda name\n                let name_id = self.interner.intern(\"<lambda>\");\n\n                // Parse lambda parameters (similar to function parameters)\n                let signature = if let Some(params) = parameters {\n                    // Parse positional-only parameters (before /)\n                    let pos_args = self.parse_params_with_defaults(&params.posonlyargs)?;\n\n                    // Parse positional-or-keyword parameters\n                    let args = self.parse_params_with_defaults(&params.args)?;\n\n                    // Parse *args\n                    let var_args = params.vararg.as_ref().map(|p| self.interner.intern(&p.name.id));\n\n                    // Parse keyword-only parameters (after * or *args)\n                    let kwargs = self.parse_params_with_defaults(&params.kwonlyargs)?;\n\n                    // Parse **kwargs\n                    let var_kwargs = params.kwarg.as_ref().map(|p| self.interner.intern(&p.name.id));\n\n                    ParsedSignature {\n                        pos_args,\n                        args,\n                        var_args,\n                        kwargs,\n                        var_kwargs,\n                    }\n                } else {\n                    // No parameters (e.g., `lambda: 42`)\n                    ParsedSignature::default()\n                };\n\n                // Parse the body expression\n                let body = Box::new(self.parse_expression(*body)?);\n\n                Ok(ExprLoc::new(\n                    position,\n                    Expr::LambdaRaw {\n                        name_id,\n                        signature,\n                        body,\n                    },\n                ))\n            }\n            AstExpr::If(ast::ExprIf {\n                test,\n                body,\n                orelse,\n                range,\n                ..\n            }) => Ok(ExprLoc::new(\n                self.convert_range(range),\n                Expr::IfElse {\n                    test: Box::new(self.parse_expression(*test)?),\n                    body: Box::new(self.parse_expression(*body)?),\n                    orelse: Box::new(self.parse_expression(*orelse)?),\n                },\n            )),\n            AstExpr::Dict(ast::ExprDict { items, range, .. }) => {\n                let position = self.convert_range(range);\n                let mut dict_items = Vec::new();\n                for ast::DictItem { key, value } in items {\n                    // key is Option<Expr> - None represents ** unpacking (PEP 448)\n                    if let Some(key_expr_ast) = key {\n                        let key_expr = self.parse_expression(key_expr_ast)?;\n                        let value_expr = self.parse_expression(value)?;\n                        dict_items.push(DictItem::Pair(key_expr, value_expr));\n                    } else {\n                        // **expr unpack in a dict literal: later keys silently win\n                        let unpack_expr = self.parse_expression(value)?;\n                        dict_items.push(DictItem::Unpack(unpack_expr));\n                    }\n                }\n                Ok(ExprLoc::new(position, Expr::Dict(dict_items)))\n            }\n            AstExpr::Set(ast::ExprSet { elts, range, .. }) => {\n                let mut items = Vec::new();\n                for e in elts {\n                    items.push(self.parse_sequence_item(e)?);\n                }\n                Ok(ExprLoc::new(self.convert_range(range), Expr::Set(items)))\n            }\n            AstExpr::ListComp(ast::ExprListComp {\n                elt, generators, range, ..\n            }) => {\n                let elt = Box::new(self.parse_expression(*elt)?);\n                let generators = self.parse_comprehension_generators(generators)?;\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::ListComp { elt, generators },\n                ))\n            }\n            AstExpr::SetComp(ast::ExprSetComp {\n                elt, generators, range, ..\n            }) => {\n                let elt = Box::new(self.parse_expression(*elt)?);\n                let generators = self.parse_comprehension_generators(generators)?;\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::SetComp { elt, generators },\n                ))\n            }\n            AstExpr::DictComp(ast::ExprDictComp {\n                key,\n                value,\n                generators,\n                range,\n                ..\n            }) => {\n                let key = Box::new(self.parse_expression(*key)?);\n                let value = Box::new(self.parse_expression(*value)?);\n                let generators = self.parse_comprehension_generators(generators)?;\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::DictComp { key, value, generators },\n                ))\n            }\n            AstExpr::Generator(ast::ExprGenerator {\n                elt, generators, range, ..\n            }) => {\n                // TODO: When proper generators are implemented, this should produce\n                // Expr::Generator instead of Expr::ListComp. Currently we treat generator\n                // expressions as list comprehensions since we don't have generator support.\n                let elt = Box::new(self.parse_expression(*elt)?);\n                let generators = self.parse_comprehension_generators(generators)?;\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::ListComp { elt, generators },\n                ))\n            }\n            AstExpr::Await(a) => {\n                let value = self.parse_expression(*a.value)?;\n                Ok(ExprLoc::new(self.convert_range(a.range), Expr::Await(Box::new(value))))\n            }\n            AstExpr::Yield(y) => Err(ParseError::not_implemented(\n                \"yield expressions\",\n                self.convert_range(y.range),\n            )),\n            AstExpr::YieldFrom(y) => Err(ParseError::not_implemented(\n                \"yield from expressions\",\n                self.convert_range(y.range),\n            )),\n            AstExpr::Compare(ast::ExprCompare {\n                left,\n                ops,\n                comparators,\n                range,\n                ..\n            }) => {\n                let position = self.convert_range(range);\n                let ops_vec = ops.into_vec();\n                let comparators_vec = comparators.into_vec();\n\n                // Simple case: single comparison (most common)\n                if ops_vec.len() == 1 {\n                    return Ok(ExprLoc::new(\n                        position,\n                        Expr::CmpOp {\n                            left: Box::new(self.parse_expression(*left)?),\n                            op: convert_compare_op(ops_vec.into_iter().next().unwrap()),\n                            right: Box::new(self.parse_expression(comparators_vec.into_iter().next().unwrap())?),\n                        },\n                    ));\n                }\n\n                // Chain comparison: transform to nested And expressions\n                self.parse_chain_comparison(*left, ops_vec, comparators_vec, position)\n            }\n            AstExpr::Call(ast::ExprCall {\n                func, arguments, range, ..\n            }) => {\n                let position = self.convert_range(range);\n                let ast::Arguments { args, keywords, .. } = arguments;\n                let args_vec = args.into_vec();\n                let keywords_vec = keywords.into_vec();\n\n                // Detect whether we need the generalized path (PEP 448):\n                // - multiple *args unpacks, OR\n                // - positional argument after *args, OR\n                // - multiple **kwargs unpacks\n                let needs_generalized = Self::needs_generalized_call(&args_vec, &keywords_vec);\n\n                let args = if needs_generalized {\n                    self.parse_generalized_call_args(args_vec, keywords_vec)?\n                } else {\n                    self.parse_simple_call_args(args_vec, keywords_vec)?\n                };\n                match *func {\n                    AstExpr::Name(ast::ExprName { id, range, .. }) => {\n                        // Always create Callable::Name — builtin resolution happens in\n                        // the prepare phase with scope awareness, so local assignments\n                        // can shadow builtins.\n                        let ident = self.identifier(&id, range);\n                        let callable = Callable::Name(ident);\n                        Ok(ExprLoc::new(\n                            position,\n                            Expr::Call {\n                                callable,\n                                args: Box::new(args),\n                            },\n                        ))\n                    }\n                    AstExpr::Attribute(ast::ExprAttribute { value, attr, .. }) => {\n                        let object = Box::new(self.parse_expression(*value)?);\n                        Ok(ExprLoc::new(\n                            position,\n                            Expr::AttrCall {\n                                object,\n                                attr: EitherStr::Interned(self.interner.intern(attr.id())),\n                                args: Box::new(args),\n                            },\n                        ))\n                    }\n                    other => {\n                        // Handle arbitrary expression as callable (e.g., lambda calls)\n                        let callable = Box::new(self.parse_expression(other)?);\n                        Ok(ExprLoc::new(\n                            position,\n                            Expr::IndirectCall {\n                                callable,\n                                args: Box::new(args),\n                            },\n                        ))\n                    }\n                }\n            }\n            AstExpr::FString(ast::ExprFString { value, range, .. }) => self.parse_fstring(&value, range),\n            AstExpr::TString(t) => Err(ParseError::not_implemented(\n                \"template strings (t-strings)\",\n                self.convert_range(t.range),\n            )),\n            AstExpr::StringLiteral(ast::ExprStringLiteral { value, range, .. }) => {\n                let string_id = self.interner.intern(&value.to_string());\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::Literal(Literal::Str(string_id)),\n                ))\n            }\n            AstExpr::BytesLiteral(ast::ExprBytesLiteral { value, range, .. }) => {\n                let bytes: Cow<'_, [u8]> = Cow::from(&value);\n                let bytes_id = self.interner.intern_bytes(&bytes);\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::Literal(Literal::Bytes(bytes_id)),\n                ))\n            }\n            AstExpr::NumberLiteral(ast::ExprNumberLiteral { value, range, .. }) => {\n                let position = self.convert_range(range);\n                let const_value = match value {\n                    Number::Int(i) => {\n                        if let Some(i) = i.as_i64() {\n                            Literal::Int(i)\n                        } else {\n                            // Integer too large for i64, parse string representation as BigInt\n                            // Handles radix prefixes (0x, 0o, 0b) and underscores\n                            let bi = parse_int_literal(&i.to_string())\n                                .ok_or_else(|| ParseError::syntax(format!(\"invalid integer literal: {i}\"), position))?;\n                            let long_int_id = self.interner.intern_long_int(bi);\n                            Literal::LongInt(long_int_id)\n                        }\n                    }\n                    Number::Float(f) => Literal::Float(f),\n                    Number::Complex { .. } => return Err(ParseError::not_implemented(\"complex constants\", position)),\n                };\n                Ok(ExprLoc::new(position, Expr::Literal(const_value)))\n            }\n            AstExpr::BooleanLiteral(ast::ExprBooleanLiteral { value, range, .. }) => Ok(ExprLoc::new(\n                self.convert_range(range),\n                Expr::Literal(Literal::Bool(value)),\n            )),\n            AstExpr::NoneLiteral(ast::ExprNoneLiteral { range, .. }) => {\n                Ok(ExprLoc::new(self.convert_range(range), Expr::Literal(Literal::None)))\n            }\n            AstExpr::EllipsisLiteral(ast::ExprEllipsisLiteral { range, .. }) => Ok(ExprLoc::new(\n                self.convert_range(range),\n                Expr::Literal(Literal::Ellipsis),\n            )),\n            AstExpr::Attribute(ast::ExprAttribute { value, attr, range, .. }) => {\n                let object = Box::new(self.parse_expression(*value)?);\n                let position = self.convert_range(range);\n                Ok(ExprLoc::new(\n                    position,\n                    Expr::AttrGet {\n                        object,\n                        attr: EitherStr::Interned(self.interner.intern(attr.id())),\n                    },\n                ))\n            }\n            AstExpr::Subscript(ast::ExprSubscript {\n                value, slice, range, ..\n            }) => {\n                let object = Box::new(self.parse_expression(*value)?);\n                let index = Box::new(self.parse_expression(*slice)?);\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::Subscript { object, index },\n                ))\n            }\n            AstExpr::Starred(s) => Err(ParseError::not_implemented(\n                \"starred expressions (*expr)\",\n                self.convert_range(s.range),\n            )),\n            AstExpr::Name(ast::ExprName { id, range, .. }) => {\n                let position = self.convert_range(range);\n                // Always create Expr::Name — builtin resolution happens in the prepare\n                // phase with scope awareness, so local assignments can shadow builtins.\n                let expr = Expr::Name(self.identifier(&id, range));\n                Ok(ExprLoc::new(position, expr))\n            }\n            AstExpr::List(ast::ExprList { elts, range, .. }) => {\n                let mut items = Vec::new();\n                for e in elts {\n                    items.push(self.parse_sequence_item(e)?);\n                }\n                Ok(ExprLoc::new(self.convert_range(range), Expr::List(items)))\n            }\n            AstExpr::Tuple(ast::ExprTuple { elts, range, .. }) => {\n                let mut items = Vec::new();\n                for e in elts {\n                    items.push(self.parse_sequence_item(e)?);\n                }\n                Ok(ExprLoc::new(self.convert_range(range), Expr::Tuple(items)))\n            }\n            AstExpr::Slice(ast::ExprSlice {\n                lower,\n                upper,\n                step,\n                range,\n                ..\n            }) => {\n                let lower = lower.map(|e| self.parse_expression(*e)).transpose()?;\n                let upper = upper.map(|e| self.parse_expression(*e)).transpose()?;\n                let step = step.map(|e| self.parse_expression(*e)).transpose()?;\n                Ok(ExprLoc::new(\n                    self.convert_range(range),\n                    Expr::Slice {\n                        lower: lower.map(Box::new),\n                        upper: upper.map(Box::new),\n                        step: step.map(Box::new),\n                    },\n                ))\n            }\n            AstExpr::IpyEscapeCommand(i) => Err(ParseError::not_implemented(\n                \"IPython escape commands\",\n                self.convert_range(i.range),\n            )),\n        }\n    }\n\n    /// Converts an AST expression into a `SequenceItem` for list/tuple/set literals.\n    ///\n    /// A `Starred` node becomes `SequenceItem::Unpack`; all other expressions\n    /// become `SequenceItem::Value`. This is the entry point for PEP 448 unpack\n    /// handling in collection literals.\n    fn parse_sequence_item(&mut self, expr: AstExpr) -> Result<SequenceItem, ParseError> {\n        if let AstExpr::Starred(ast::ExprStarred { value, .. }) = expr {\n            Ok(SequenceItem::Unpack(self.parse_expression(*value)?))\n        } else {\n            Ok(SequenceItem::Value(self.parse_expression(expr)?))\n        }\n    }\n\n    /// Detects whether a function call needs the generalized `GeneralizedCall` path.\n    ///\n    /// Returns `true` when the call has:\n    /// - More than one `*unpack` among positional args, OR\n    /// - A plain positional arg following a `*unpack`, OR\n    /// - More than one `**unpack` among keyword args.\n    ///\n    /// In all these cases the simple `ArgsKargs` representation is insufficient\n    /// and `parse_generalized_call_args` must be used instead.\n    fn needs_generalized_call(args: &[AstExpr], keywords: &[Keyword]) -> bool {\n        let mut seen_star = false;\n        for arg in args {\n            match arg {\n                AstExpr::Starred(_) => {\n                    if seen_star {\n                        return true; // second *unpack\n                    }\n                    seen_star = true;\n                }\n                _ => {\n                    if seen_star {\n                        return true; // positional after *unpack\n                    }\n                }\n            }\n        }\n        // Multiple **kwargs unpacks?\n        keywords.iter().filter(|k| k.arg.is_none()).count() > 1\n    }\n\n    /// Parses function call args for the simple case (at most one * and one **).\n    ///\n    /// Returns `ArgExprs::new_with_var_kwargs(...)` as before, preserving the\n    /// fast path for the vast majority of function calls.\n    fn parse_simple_call_args(\n        &mut self,\n        args_vec: Vec<AstExpr>,\n        keywords_vec: Vec<Keyword>,\n    ) -> Result<ArgExprs, ParseError> {\n        let mut positional_args = Vec::new();\n        let mut var_args_expr: Option<ExprLoc> = None;\n\n        for arg_expr in args_vec {\n            match arg_expr {\n                AstExpr::Starred(ast::ExprStarred { value, .. }) => {\n                    var_args_expr = Some(self.parse_expression(*value)?);\n                }\n                other => {\n                    positional_args.push(self.parse_expression(other)?);\n                }\n            }\n        }\n        let (kwargs, var_kwargs) = self.parse_keywords(keywords_vec)?;\n        Ok(ArgExprs::new_with_var_kwargs(\n            positional_args,\n            var_args_expr,\n            kwargs,\n            var_kwargs,\n        ))\n    }\n\n    /// Parses function call args for the PEP 448 generalized case.\n    ///\n    /// Builds `Vec<CallArg>` and `Vec<CallKwarg>` preserving the full order of\n    /// positional and keyword arguments so the compiler can emit correct\n    /// `ListAppend`/`ListExtend`/`DictMerge` sequences.\n    fn parse_generalized_call_args(\n        &mut self,\n        args_vec: Vec<AstExpr>,\n        keywords_vec: Vec<Keyword>,\n    ) -> Result<ArgExprs, ParseError> {\n        let mut call_args = Vec::new();\n        for arg_expr in args_vec {\n            match arg_expr {\n                AstExpr::Starred(ast::ExprStarred { value, .. }) => {\n                    call_args.push(CallArg::Unpack(self.parse_expression(*value)?));\n                }\n                other => {\n                    call_args.push(CallArg::Value(self.parse_expression(other)?));\n                }\n            }\n        }\n\n        let mut call_kwargs = Vec::new();\n        for kwarg in keywords_vec {\n            if let Some(key) = kwarg.arg {\n                let key_ident = self.identifier(&key.id, key.range);\n                let value = self.parse_expression(kwarg.value)?;\n                call_kwargs.push(CallKwarg::Named(Kwarg { key: key_ident, value }));\n            } else {\n                let unpack_expr = self.parse_expression(kwarg.value)?;\n                call_kwargs.push(CallKwarg::Unpack(unpack_expr));\n            }\n        }\n\n        Ok(ArgExprs::new_generalized(call_args, call_kwargs))\n    }\n\n    /// Parses keyword arguments, separating regular kwargs from var_kwargs (`**expr`).\n    ///\n    /// Returns `(kwargs, var_kwargs)` where kwargs is a vec of named keyword arguments\n    /// and var_kwargs is an optional expression for `**expr` unpacking.\n    fn parse_keywords(&mut self, keywords: Vec<Keyword>) -> Result<(Vec<Kwarg>, Option<ExprLoc>), ParseError> {\n        let mut kwargs = Vec::new();\n        let mut var_kwargs = None;\n\n        for kwarg in keywords {\n            if let Some(key) = kwarg.arg {\n                // Regular kwarg: key=value\n                let key = self.identifier(&key.id, key.range);\n                let value = self.parse_expression(kwarg.value)?;\n                kwargs.push(Kwarg { key, value });\n            } else {\n                // Var kwargs: **expr\n                if var_kwargs.is_some() {\n                    return Err(ParseError::not_implemented(\n                        \"multiple **kwargs unpacking\",\n                        self.convert_range(kwarg.range),\n                    ));\n                }\n                var_kwargs = Some(self.parse_expression(kwarg.value)?);\n            }\n        }\n\n        Ok((kwargs, var_kwargs))\n    }\n\n    fn parse_identifier(&mut self, ast: AstExpr) -> Result<Identifier, ParseError> {\n        match ast {\n            AstExpr::Name(ast::ExprName { id, range, .. }) => Ok(self.identifier(&id, range)),\n            other => Err(ParseError::syntax(\n                format!(\"Expected name, got {other:?}\"),\n                self.convert_range(other.range()),\n            )),\n        }\n    }\n\n    /// Parses a chain comparison expression like `a < b < c < d`.\n    ///\n    /// Chain comparisons evaluate each intermediate value only once and short-circuit\n    /// on the first false result. This creates an `Expr::ChainCmp` node which is\n    /// compiled to bytecode using stack manipulation (Dup, Rot) rather than\n    /// temporary variables, avoiding namespace pollution.\n    fn parse_chain_comparison(\n        &mut self,\n        left: AstExpr,\n        ops: Vec<CmpOp>,\n        comparators: Vec<AstExpr>,\n        position: CodeRange,\n    ) -> Result<ExprLoc, ParseError> {\n        let left_expr = self.parse_expression(left)?;\n        let comparisons = ops\n            .into_iter()\n            .zip(comparators)\n            .map(|(op, cmp)| Ok((convert_compare_op(op), self.parse_expression(cmp)?)))\n            .collect::<Result<Vec<_>, ParseError>>()?;\n\n        Ok(ExprLoc::new(\n            position,\n            Expr::ChainCmp {\n                left: Box::new(left_expr),\n                comparisons,\n            },\n        ))\n    }\n\n    /// Parses an unpack target - either a single identifier or a nested tuple.\n    ///\n    /// Handles patterns like `a` (single variable), `a, b` (flat tuple), or `(a, b), c` (nested).\n    /// Includes depth tracking to prevent stack overflow from deeply nested structures.\n    fn parse_unpack_target(&mut self, ast: AstExpr) -> Result<UnpackTarget, ParseError> {\n        self.decr_depth_remaining(|| ast.range())?;\n        let result = self.parse_unpack_target_impl(ast);\n        self.depth_remaining += 1;\n        result\n    }\n\n    fn parse_unpack_target_impl(&mut self, ast: AstExpr) -> Result<UnpackTarget, ParseError> {\n        match ast {\n            AstExpr::Name(ast::ExprName { id, range, .. }) => Ok(UnpackTarget::Name(self.identifier(&id, range))),\n            AstExpr::Tuple(ast::ExprTuple { elts, range, .. }) => {\n                let position = self.convert_range(range);\n                let targets = elts\n                    .into_iter()\n                    .map(|e| self.parse_unpack_target(e)) // Recursive call for nested tuples\n                    .collect::<Result<Vec<_>, _>>()?;\n                if targets.is_empty() {\n                    return Err(ParseError::syntax(\"empty tuple in unpack target\", position));\n                }\n                // Validate at most one starred target\n                let starred_count = targets.iter().filter(|t| matches!(t, UnpackTarget::Starred(_))).count();\n                if starred_count > 1 {\n                    return Err(ParseError::syntax(\n                        \"multiple starred expressions in assignment\",\n                        position,\n                    ));\n                }\n                Ok(UnpackTarget::Tuple { targets, position })\n            }\n            AstExpr::Starred(ast::ExprStarred { value, range, .. }) => {\n                // Starred target must be a simple name\n                match *value {\n                    AstExpr::Name(ast::ExprName { id, range, .. }) => {\n                        Ok(UnpackTarget::Starred(self.identifier(&id, range)))\n                    }\n                    _ => Err(ParseError::syntax(\n                        \"starred assignment target must be a name\",\n                        self.convert_range(range),\n                    )),\n                }\n            }\n            AstExpr::List(ast::ExprList { elts, range, .. }) => {\n                // List unpacking target [a, b, *rest] - same as tuple\n                let position = self.convert_range(range);\n                let targets = elts\n                    .into_iter()\n                    .map(|e| self.parse_unpack_target(e))\n                    .collect::<Result<Vec<_>, _>>()?;\n                if targets.is_empty() {\n                    return Err(ParseError::syntax(\"empty list in unpack target\", position));\n                }\n                // Validate at most one starred target\n                let starred_count = targets.iter().filter(|t| matches!(t, UnpackTarget::Starred(_))).count();\n                if starred_count > 1 {\n                    return Err(ParseError::syntax(\n                        \"multiple starred expressions in assignment\",\n                        position,\n                    ));\n                }\n                Ok(UnpackTarget::Tuple { targets, position })\n            }\n            other => Err(ParseError::syntax(\n                format!(\"invalid unpacking target: {other:?}\"),\n                self.convert_range(other.range()),\n            )),\n        }\n    }\n\n    fn identifier(&mut self, id: &Name, range: TextRange) -> Identifier {\n        let string_id = self.interner.intern(id);\n        Identifier::new(string_id, self.convert_range(range))\n    }\n\n    /// Parses function parameters with optional default values.\n    ///\n    /// Handles parameters like `a`, `b=10`, `c=None` by extracting the parameter\n    /// name and parsing any default expression. Default expressions are stored\n    /// as unevaluated AST and will be evaluated during the prepare phase.\n    fn parse_params_with_defaults(&mut self, params: &[ParameterWithDefault]) -> Result<Vec<ParsedParam>, ParseError> {\n        params\n            .iter()\n            .map(|p| {\n                let name = self.interner.intern(&p.parameter.name.id);\n                let default = match &p.default {\n                    Some(expr) => Some(self.parse_expression((**expr).clone())?),\n                    None => None,\n                };\n                Ok(ParsedParam { name, default })\n            })\n            .collect()\n    }\n\n    /// Parses comprehension generators (the `for ... in ... if ...` clauses).\n    ///\n    /// Each generator represents one `for` clause with zero or more `if` filters.\n    /// Multiple generators create nested iteration. Supports both single identifiers\n    /// (`for x in ...`) and tuple unpacking (`for x, y in ...`).\n    fn parse_comprehension_generators(\n        &mut self,\n        generators: Vec<ast::Comprehension>,\n    ) -> Result<Vec<Comprehension>, ParseError> {\n        generators\n            .into_iter()\n            .map(|comp| {\n                if comp.is_async {\n                    return Err(ParseError::not_implemented(\n                        \"async comprehensions\",\n                        self.convert_range(comp.range),\n                    ));\n                }\n                let target = self.parse_unpack_target(comp.target)?;\n                let iter = self.parse_expression(comp.iter)?;\n                let ifs = comp\n                    .ifs\n                    .into_iter()\n                    .map(|cond| self.parse_expression(cond))\n                    .collect::<Result<Vec<_>, _>>()?;\n                Ok(Comprehension { target, iter, ifs })\n            })\n            .collect()\n    }\n\n    /// Parses an f-string value into expression parts.\n    ///\n    /// F-strings in ruff AST are represented as `FStringValue` containing\n    /// `FStringPart`s, which can be either literal strings or `FString`\n    /// interpolated sections. Each `FString` contains `InterpolatedStringElements`.\n    fn parse_fstring(&mut self, value: &ast::FStringValue, range: TextRange) -> Result<ExprLoc, ParseError> {\n        let mut parts = Vec::new();\n\n        for fstring_part in value {\n            match fstring_part {\n                ast::FStringPart::Literal(lit) => {\n                    // Literal string segment - intern for use at runtime\n                    let processed = lit.value.to_string();\n                    if !processed.is_empty() {\n                        let string_id = self.interner.intern(&processed);\n                        parts.push(FStringPart::Literal(string_id));\n                    }\n                }\n                ast::FStringPart::FString(fstring) => {\n                    // Interpolated f-string section\n                    for element in &fstring.elements {\n                        let part = self.parse_fstring_element(element)?;\n                        parts.push(part);\n                    }\n                }\n            }\n        }\n\n        // Optimization: if only one literal part, return as simple string literal\n        if parts.len() == 1\n            && let FStringPart::Literal(string_id) = parts[0]\n        {\n            return Ok(ExprLoc::new(\n                self.convert_range(range),\n                Expr::Literal(Literal::Str(string_id)),\n            ));\n        }\n\n        Ok(ExprLoc::new(self.convert_range(range), Expr::FString(parts)))\n    }\n\n    /// Parses a single f-string element (literal or interpolation).\n    fn parse_fstring_element(&mut self, element: &InterpolatedStringElement) -> Result<FStringPart, ParseError> {\n        match element {\n            InterpolatedStringElement::Literal(lit) => {\n                // Intern the literal string for use at runtime\n                let processed = lit.value.to_string();\n                let string_id = self.interner.intern(&processed);\n                Ok(FStringPart::Literal(string_id))\n            }\n            InterpolatedStringElement::Interpolation(interp) => {\n                let expr = Box::new(self.parse_expression((*interp.expression).clone())?);\n                let conversion = convert_conversion_flag(interp.conversion);\n                let format_spec = match &interp.format_spec {\n                    Some(spec) => Some(self.parse_format_spec(spec)?),\n                    None => None,\n                };\n                // Extract debug prefix for `=` specifier (e.g., f'{a=}' -> \"a=\")\n                let debug_prefix = interp.debug_text.as_ref().map(|dt| {\n                    let expr_text = &self.code[interp.expression.range()];\n                    self.interner\n                        .intern(&format!(\"{}{}{}\", dt.leading, expr_text, dt.trailing))\n                });\n                Ok(FStringPart::Interpolation {\n                    expr,\n                    conversion,\n                    format_spec,\n                    debug_prefix,\n                })\n            }\n        }\n    }\n\n    /// Parses a format specification, which may contain nested interpolations.\n    ///\n    /// For static specs (no interpolations), parses the format string into a\n    /// `ParsedFormatSpec` at parse time to avoid runtime parsing overhead.\n    fn parse_format_spec(&mut self, spec: &ast::InterpolatedStringFormatSpec) -> Result<FormatSpec, ParseError> {\n        let mut parts = Vec::new();\n        let mut has_interpolation = false;\n\n        for element in &spec.elements {\n            match element {\n                InterpolatedStringElement::Literal(lit) => {\n                    // Intern the literal string\n                    let processed = lit.value.to_string();\n                    let string_id = self.interner.intern(&processed);\n                    parts.push(FStringPart::Literal(string_id));\n                }\n                InterpolatedStringElement::Interpolation(interp) => {\n                    has_interpolation = true;\n                    let expr = Box::new(self.parse_expression((*interp.expression).clone())?);\n                    let conversion = convert_conversion_flag(interp.conversion);\n                    // Format specs within format specs are not allowed in Python,\n                    // and debug_prefix doesn't apply to nested interpolations\n                    parts.push(FStringPart::Interpolation {\n                        expr,\n                        conversion,\n                        format_spec: None,\n                        debug_prefix: None,\n                    });\n                }\n            }\n        }\n\n        if has_interpolation {\n            Ok(FormatSpec::Dynamic(parts))\n        } else {\n            // Combine all literal parts into a single static string and parse at parse time\n            let static_spec: String = parts\n                .into_iter()\n                .filter_map(|p| {\n                    if let FStringPart::Literal(string_id) = p {\n                        Some(self.interner.get_str(string_id).to_owned())\n                    } else {\n                        None\n                    }\n                })\n                .collect();\n            let parsed = static_spec.parse().map_err(|spec_str| {\n                ParseError::syntax(\n                    format!(\"Invalid format specifier '{spec_str}'\"),\n                    self.convert_range(spec.range),\n                )\n            })?;\n            Ok(FormatSpec::Static(parsed))\n        }\n    }\n\n    fn convert_range(&self, range: TextRange) -> CodeRange {\n        let start = range.start().into();\n        let (start_line_no, start_line_start, _) = self.index_to_position(start);\n        let start = CodeLoc::new(start_line_no, start - start_line_start);\n\n        let end = range.end().into();\n        let (end_line_no, end_line_start, _) = self.index_to_position(end);\n        let end = CodeLoc::new(end_line_no, end - end_line_start);\n\n        // Store line number for single-line ranges, None for multi-line\n        let preview_line = if start_line_no == end_line_no {\n            Some(u32::try_from(start_line_no).expect(\"line number exceeds u32\"))\n        } else {\n            None\n        };\n\n        CodeRange::new(self.filename_id, start, end, preview_line)\n    }\n\n    fn index_to_position(&self, index: usize) -> (usize, usize, Option<usize>) {\n        let mut line_start = 0;\n        for (line_no, line_end) in self.line_ends.iter().enumerate() {\n            if index <= *line_end {\n                return (line_no, line_start, Some(*line_end));\n            }\n            line_start = *line_end + 1;\n        }\n        // Content after the last newline (file without trailing newline)\n        // line_ends.len() gives the correct 0-indexed line number\n        (self.line_ends.len(), line_start, None)\n    }\n\n    /// Decrements the depth remaining for nested parentheses.\n    /// Returns an error if the depth remaining goes to zero.\n    fn decr_depth_remaining(&mut self, get_range: impl FnOnce() -> TextRange) -> Result<(), ParseError> {\n        if let Some(depth_remaining) = self.depth_remaining.checked_sub(1) {\n            self.depth_remaining = depth_remaining;\n            Ok(())\n        } else {\n            let position = self.convert_range(get_range());\n            Err(ParseError::syntax(\"too many nested parentheses\", position))\n        }\n    }\n}\n\nfn first<T: fmt::Debug>(v: Vec<T>, position: CodeRange) -> Result<T, ParseError> {\n    if v.len() == 1 {\n        v.into_iter()\n            .next()\n            .ok_or_else(|| ParseError::syntax(\"Expected 1 element, got 0\", position))\n    } else {\n        Err(ParseError::syntax(\n            format!(\"Expected 1 element, got {} (raw: {v:?})\", v.len()),\n            position,\n        ))\n    }\n}\n\nfn convert_op(op: AstOperator) -> Operator {\n    match op {\n        AstOperator::Add => Operator::Add,\n        AstOperator::Sub => Operator::Sub,\n        AstOperator::Mult => Operator::Mult,\n        AstOperator::MatMult => Operator::MatMult,\n        AstOperator::Div => Operator::Div,\n        AstOperator::Mod => Operator::Mod,\n        AstOperator::Pow => Operator::Pow,\n        AstOperator::LShift => Operator::LShift,\n        AstOperator::RShift => Operator::RShift,\n        AstOperator::BitOr => Operator::BitOr,\n        AstOperator::BitXor => Operator::BitXor,\n        AstOperator::BitAnd => Operator::BitAnd,\n        AstOperator::FloorDiv => Operator::FloorDiv,\n    }\n}\n\nfn convert_bool_op(op: BoolOp) -> Operator {\n    match op {\n        BoolOp::And => Operator::And,\n        BoolOp::Or => Operator::Or,\n    }\n}\n\nfn convert_compare_op(op: CmpOp) -> CmpOperator {\n    match op {\n        CmpOp::Eq => CmpOperator::Eq,\n        CmpOp::NotEq => CmpOperator::NotEq,\n        CmpOp::Lt => CmpOperator::Lt,\n        CmpOp::LtE => CmpOperator::LtE,\n        CmpOp::Gt => CmpOperator::Gt,\n        CmpOp::GtE => CmpOperator::GtE,\n        CmpOp::Is => CmpOperator::Is,\n        CmpOp::IsNot => CmpOperator::IsNot,\n        CmpOp::In => CmpOperator::In,\n        CmpOp::NotIn => CmpOperator::NotIn,\n    }\n}\n\n/// Converts ruff's ConversionFlag to our ConversionFlag.\nfn convert_conversion_flag(flag: RuffConversionFlag) -> ConversionFlag {\n    match flag {\n        RuffConversionFlag::None => ConversionFlag::None,\n        RuffConversionFlag::Str => ConversionFlag::Str,\n        RuffConversionFlag::Repr => ConversionFlag::Repr,\n        RuffConversionFlag::Ascii => ConversionFlag::Ascii,\n    }\n}\n\n/// Source code location information for error reporting.\n///\n/// Contains filename (as StringId), line/column positions, and optionally a line number for\n/// extracting the preview line from source during traceback formatting.\n///\n/// To display the filename, the caller must provide access to the string storage.\n#[derive(Clone, Copy, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]\npub struct CodeRange {\n    /// Interned filename ID - look up in Interns to get the actual string.\n    pub filename: StringId,\n    /// Line number (0-indexed) for extracting preview from source. None if range spans multiple lines.\n    preview_line: Option<u32>,\n    start: CodeLoc,\n    end: CodeLoc,\n}\n\n/// Custom Debug implementation to make displaying code much less verbose.\nimpl fmt::Debug for CodeRange {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(\n            f,\n            \"CodeRange{{filename: {:?}, start: {:?}, end: {:?}}}\",\n            self.filename, self.start, self.end\n        )\n    }\n}\n\nimpl CodeRange {\n    fn new(filename: StringId, start: CodeLoc, end: CodeLoc, preview_line: Option<u32>) -> Self {\n        Self {\n            filename,\n            preview_line,\n            start,\n            end,\n        }\n    }\n\n    /// Returns the start position.\n    #[must_use]\n    pub fn start(&self) -> CodeLoc {\n        self.start\n    }\n\n    /// Returns the end position.\n    #[must_use]\n    pub fn end(&self) -> CodeLoc {\n        self.end\n    }\n\n    /// Returns the preview line number (0-indexed) if available.\n    #[must_use]\n    pub fn preview_line_number(&self) -> Option<u32> {\n        self.preview_line\n    }\n}\n\n/// Errors that can occur during parsing or preparation of Python code.\n#[derive(Debug, Clone)]\npub enum ParseError {\n    /// Error in syntax\n    Syntax {\n        msg: Cow<'static, str>,\n        position: CodeRange,\n    },\n    /// Missing feature from Monty, we hope to implement in the future.\n    /// Message gets prefixed with \"The monty syntax parser does not yet support \".\n    NotImplemented {\n        msg: Cow<'static, str>,\n        position: CodeRange,\n    },\n    /// Missing feature with a custom full message (no prefix added).\n    NotSupported {\n        msg: Cow<'static, str>,\n        position: CodeRange,\n    },\n    /// Import error (e.g., relative imports without a package).\n    Import {\n        msg: Cow<'static, str>,\n        position: CodeRange,\n    },\n}\n\nimpl ParseError {\n    fn not_implemented(msg: impl Into<Cow<'static, str>>, position: CodeRange) -> Self {\n        Self::NotImplemented {\n            msg: msg.into(),\n            position,\n        }\n    }\n\n    fn not_supported(msg: impl Into<Cow<'static, str>>, position: CodeRange) -> Self {\n        Self::NotSupported {\n            msg: msg.into(),\n            position,\n        }\n    }\n\n    fn import_error(msg: impl Into<Cow<'static, str>>, position: CodeRange) -> Self {\n        Self::Import {\n            msg: msg.into(),\n            position,\n        }\n    }\n\n    pub(crate) fn syntax(msg: impl Into<Cow<'static, str>>, position: CodeRange) -> Self {\n        Self::Syntax {\n            msg: msg.into(),\n            position,\n        }\n    }\n}\n\nimpl ParseError {\n    pub fn into_python_exc(self, filename: &str, source: &str) -> MontyException {\n        match self {\n            Self::Syntax { msg, position } => MontyException::new_full(\n                ExcType::SyntaxError,\n                Some(msg.into_owned()),\n                vec![StackFrame::from_position_syntax_error(position, filename, source)],\n            ),\n            Self::NotImplemented { msg, position } => MontyException::new_full(\n                ExcType::NotImplementedError,\n                Some(format!(\"The monty syntax parser does not yet support {msg}\")),\n                vec![StackFrame::from_position(position, filename, source)],\n            ),\n            Self::NotSupported { msg, position } => MontyException::new_full(\n                ExcType::NotImplementedError,\n                Some(msg.into_owned()),\n                vec![StackFrame::from_position(position, filename, source)],\n            ),\n            Self::Import { msg, position } => MontyException::new_full(\n                ExcType::ImportError,\n                Some(msg.into_owned()),\n                vec![StackFrame::from_position_no_caret(position, filename, source)],\n            ),\n        }\n    }\n}\n\n/// Parses an integer literal string into a `BigInt`, handling radix prefixes and underscores.\n///\n/// Supports Python integer literal formats:\n/// - Decimal: `123`, `1_000_000`\n/// - Hexadecimal: `0x1a2b`, `0X1A2B`\n/// - Octal: `0o777`, `0O777`\n/// - Binary: `0b1010`, `0B1010`\n///\n/// Returns `None` if the string cannot be parsed.\nfn parse_int_literal(s: &str) -> Option<BigInt> {\n    // Remove underscores (Python allows them as digit separators)\n    let cleaned: String = s.chars().filter(|c| *c != '_').collect();\n    let cleaned = cleaned.as_str();\n\n    // Detect radix from prefix\n    if cleaned.len() >= 2 {\n        let prefix = &cleaned[..2];\n        let digits = &cleaned[2..];\n        match prefix.to_ascii_lowercase().as_str() {\n            \"0x\" => return BigInt::parse_bytes(digits.as_bytes(), 16),\n            \"0o\" => return BigInt::parse_bytes(digits.as_bytes(), 8),\n            \"0b\" => return BigInt::parse_bytes(digits.as_bytes(), 2),\n            _ => {}\n        }\n    }\n\n    // Default to decimal\n    cleaned.parse::<BigInt>().ok()\n}\n"
  },
  {
    "path": "crates/monty/src/prepare.rs",
    "content": "use std::collections::hash_map::Entry;\n\nuse ahash::{AHashMap, AHashSet};\n\nuse crate::{\n    args::{ArgExprs, CallArg, CallKwarg},\n    builtins::Builtins,\n    expressions::{\n        Callable, CmpOperator, Comprehension, DictItem, Expr, ExprLoc, Identifier, Literal, NameScope, Node, Operator,\n        PreparedFunctionDef, PreparedNode, SequenceItem, UnpackTarget,\n    },\n    fstring::{FStringPart, FormatSpec},\n    intern::{InternerBuilder, StringId},\n    namespace::NamespaceId,\n    parse::{CodeRange, ExceptHandler, ParseError, ParseNode, ParseResult, ParsedSignature, RawFunctionDef, Try},\n    signature::Signature,\n};\n\n/// Result of the prepare phase, containing everything needed to compile and execute code.\n///\n/// This struct holds the outputs of name resolution and AST transformation:\n/// - The namespace size (number of slots needed at module level)\n/// - A mapping from variable names to their namespace indices (for ref-count testing)\n/// - The transformed AST nodes with all names resolved, ready for compilation\n/// - The string interner containing all interned identifiers and filenames\npub struct PrepareResult {\n    /// Number of items in the namespace (at module level, this IS the global namespace)\n    pub namespace_size: usize,\n    /// Maps variable names to their indices in the namespace.\n    ///\n    /// This map is used by:\n    /// - ref-count tests for looking up variables by name\n    /// - REPL incremental compilation to preserve stable global slot IDs across snippets\n    pub name_map: AHashMap<String, NamespaceId>,\n    /// The prepared AST nodes with all names resolved to namespace indices.\n    /// Function definitions are inline as `PreparedFunctionDef` variants.\n    pub nodes: Vec<PreparedNode>,\n    /// The string interner containing all interned identifiers and filenames.\n    pub interner: InternerBuilder,\n}\n\n/// Prepares parsed nodes for compilation by resolving names and building the initial namespace.\n///\n/// The namespace will be converted to runtime Objects when execution begins and the heap is available.\n/// At module level, the local namespace IS the global namespace.\npub(crate) fn prepare(parse_result: ParseResult, input_names: Vec<String>) -> Result<PrepareResult, ParseError> {\n    let ParseResult { nodes, interner } = parse_result;\n    let mut p = Prepare::new_module(input_names, &interner);\n    let mut prepared_nodes = p.prepare_nodes(nodes)?;\n\n    // In the root frame, the last expression is implicitly returned\n    // if it's not None. This matches Python REPL behavior where the last expression\n    // value is displayed/returned.\n    if let Some(Node::Expr(expr_loc)) = prepared_nodes.last()\n        && !expr_loc.expr.is_none()\n    {\n        let new_expr_loc = expr_loc.clone();\n        prepared_nodes.pop();\n        prepared_nodes.push(Node::Return(new_expr_loc));\n    }\n\n    Ok(PrepareResult {\n        namespace_size: p.namespace_size,\n        name_map: p.name_map,\n        nodes: prepared_nodes,\n        interner,\n    })\n}\n\n/// Prepares parsed nodes for REPL-style incremental compilation using an existing global namespace map.\n///\n/// Existing bindings keep their original namespace slots; any new names are appended with new slots.\n/// This ensures snippets can be compiled independently while sharing one persistent global namespace.\npub(crate) fn prepare_with_existing_names(\n    parse_result: ParseResult,\n    existing_name_map: AHashMap<String, NamespaceId>,\n) -> Result<PrepareResult, ParseError> {\n    let ParseResult { nodes, interner } = parse_result;\n    let mut p = Prepare::new_module_with_name_map(existing_name_map, &interner);\n    let mut prepared_nodes = p.prepare_nodes(nodes)?;\n\n    // In the root frame, the last expression is implicitly returned to match REPL behavior.\n    if let Some(Node::Expr(expr_loc)) = prepared_nodes.last()\n        && !expr_loc.expr.is_none()\n    {\n        let new_expr_loc = expr_loc.clone();\n        prepared_nodes.pop();\n        prepared_nodes.push(Node::Return(new_expr_loc));\n    }\n\n    Ok(PrepareResult {\n        namespace_size: p.namespace_size,\n        name_map: p.name_map,\n        nodes: prepared_nodes,\n        interner,\n    })\n}\n\n/// State machine for the preparation phase that transforms parsed AST nodes into a prepared form.\n///\n/// This struct maintains the mapping between variable names and their namespace indices,\n/// and handles scope resolution. The preparation phase is crucial for converting string-based\n/// name lookups into efficient integer-indexed namespace access during compilation and execution.\n///\n/// For functions, this struct also tracks:\n/// - Which variables are declared `global` (should resolve to module namespace)\n/// - Which variables are declared `nonlocal` (should resolve to enclosing scope via cells)\n/// - Which variables are assigned locally (determines local vs global scope)\n/// - Reference to the global name map for resolving global variable references\n/// - Enclosing scope information for closure analysis\nstruct Prepare<'i> {\n    /// Reference to the string interner for looking up names in error messages.\n    interner: &'i InternerBuilder,\n    /// Maps variable names to their indices in this scope's namespace vector\n    name_map: AHashMap<String, NamespaceId>,\n    /// Number of items in the namespace\n    pub namespace_size: usize,\n    /// Whether this is the module-level scope.\n    /// At module level, all variables are global and `global` keyword is a no-op.\n    is_module_scope: bool,\n    /// Names declared as `global` in this scope.\n    /// These names will resolve to the global namespace instead of local.\n    global_names: AHashSet<String>,\n    /// Names that are assigned in this scope (from first-pass scan).\n    /// Used in functions to determine if a variable is local (assigned) or global (only read).\n    assigned_names: AHashSet<String>,\n    /// Names that have been assigned so far during the second pass (in order).\n    /// Used to produce the correct error message for `global x` when x was assigned before.\n    names_assigned_in_order: AHashSet<String>,\n    /// Copy of the module-level global name map.\n    /// Used by functions to resolve global variable references.\n    /// None at module level (not needed since all names are global there).\n    global_name_map: Option<AHashMap<String, NamespaceId>>,\n    /// Names that exist as locals in the enclosing function scope.\n    /// Used to validate `nonlocal` declarations and resolve captured variables.\n    /// None at module level or when there's no enclosing function.\n    enclosing_locals: Option<AHashSet<String>>,\n    /// Maps free variable names (from nonlocal declarations and implicit captures) to their\n    /// index in the free_vars vector. Pre-populated with nonlocal names at initialization,\n    /// then extended with implicit captures discovered during preparation.\n    free_var_map: AHashMap<String, NamespaceId>,\n    /// Maps cell variable names to their index in the owned_cells vector.\n    /// Pre-populated with cell_var names at initialization (excluding pass-through variables\n    /// that are both nonlocal and captured by nested functions), then extended as new\n    /// captures are discovered during nested function preparation.\n    cell_var_map: AHashMap<String, NamespaceId>,\n    /// Names that were resolved as `LocalUnassigned` in step 8 of `get_id`.\n    ///\n    /// These names are never assigned and not parameters - they were only referenced\n    /// (e.g., external function names). Tracking them prevents step 6 from incorrectly\n    /// classifying subsequent references as `Local` (like parameters) when the name\n    /// appears in `name_map` from a previous `get_id` call.\n    unassigned_ref_names: AHashSet<String>,\n}\n\nimpl<'i> Prepare<'i> {\n    /// Creates a new Prepare instance for module-level code.\n    ///\n    /// At module level, all variables are global. The `global` keyword is a no-op\n    /// since all variables are already in the global namespace.\n    ///\n    /// # Arguments\n    /// * `input_names` - Names that should be pre-registered in the namespace (e.g., input variables)\n    /// * `interner` - Reference to the string interner for looking up names\n    fn new_module(input_names: Vec<String>, interner: &'i InternerBuilder) -> Self {\n        let mut name_map = AHashMap::with_capacity(input_names.len());\n        for (index, name) in input_names.into_iter().enumerate() {\n            name_map.insert(name, NamespaceId::new(index));\n        }\n        let namespace_size = name_map.len();\n        Self {\n            interner,\n            name_map,\n            namespace_size,\n            is_module_scope: true,\n            global_names: AHashSet::new(),\n            assigned_names: AHashSet::new(),\n            names_assigned_in_order: AHashSet::new(),\n            global_name_map: None,\n            enclosing_locals: None,\n            free_var_map: AHashMap::new(),\n            cell_var_map: AHashMap::new(),\n            unassigned_ref_names: AHashSet::new(),\n        }\n    }\n\n    /// Creates a module-scope Prepare instance from an existing global name map.\n    ///\n    /// Used by incremental REPL compilation to keep stable slot assignments across snippets.\n    fn new_module_with_name_map(name_map: AHashMap<String, NamespaceId>, interner: &'i InternerBuilder) -> Self {\n        let namespace_size = name_map\n            .values()\n            .map(|id| id.index())\n            .max()\n            .map_or(0, |max_idx| max_idx + 1);\n\n        Self {\n            interner,\n            name_map,\n            namespace_size,\n            is_module_scope: true,\n            global_names: AHashSet::new(),\n            assigned_names: AHashSet::new(),\n            names_assigned_in_order: AHashSet::new(),\n            global_name_map: None,\n            enclosing_locals: None,\n            free_var_map: AHashMap::new(),\n            cell_var_map: AHashMap::new(),\n            unassigned_ref_names: AHashSet::new(),\n        }\n    }\n\n    /// Creates a new Prepare instance for function-level code.\n    ///\n    /// Pre-populates `free_var_map` with nonlocal declarations and implicit captures,\n    /// and `cell_var_map` with cell variables (excluding pass-through variables).\n    ///\n    /// # Arguments\n    /// * `capacity` - Expected number of nodes\n    /// * `params` - Function parameter StringIds (pre-registered in namespace)\n    /// * `assigned_names` - Names that are assigned in this function (from first-pass scan)\n    /// * `global_names` - Names declared as `global` in this function\n    /// * `nonlocal_names` - Names declared as `nonlocal` in this function\n    /// * `implicit_captures` - Names captured from enclosing scope without explicit nonlocal\n    /// * `global_name_map` - Copy of the module-level name map for global resolution\n    /// * `enclosing_locals` - Names that exist as locals in the enclosing function (for nonlocal resolution)\n    /// * `cell_var_names` - Names that are captured by nested functions (must be stored in cells)\n    /// * `interner` - Reference to the string interner for looking up names\n    #[expect(clippy::too_many_arguments)]\n    fn new_function(\n        capacity: usize,\n        params: &[StringId],\n        assigned_names: AHashSet<String>,\n        global_names: AHashSet<String>,\n        nonlocal_names: AHashSet<String>,\n        implicit_captures: AHashSet<String>,\n        global_name_map: AHashMap<String, NamespaceId>,\n        enclosing_locals: Option<AHashSet<String>>,\n        cell_var_names: AHashSet<String>,\n        interner: &'i InternerBuilder,\n    ) -> Self {\n        let mut name_map = AHashMap::with_capacity(capacity);\n        for (index, string_id) in params.iter().enumerate() {\n            name_map.insert(interner.get_str(*string_id).to_string(), NamespaceId::new(index));\n        }\n        let namespace_size = name_map.len();\n\n        // Namespace layout: [params][cell_vars][free_vars][locals]\n        // This predictable layout allows sequential namespace construction at runtime.\n\n        // Pre-populate cell_var_map with cell variables FIRST (right after params).\n        // Excludes pass-through variables (names that are both nonlocal and captured by\n        // nested functions - these stay in free_var_map since we receive the cell, not create it).\n        // NOTE: We intentionally do NOT add these to name_map here, because the scope\n        // validation checks name_map to detect \"used before declaration\" errors\n        let mut cell_var_map = AHashMap::with_capacity(cell_var_names.len());\n        let mut namespace_size = namespace_size;\n        for name in cell_var_names {\n            if !nonlocal_names.contains(&name) && !implicit_captures.contains(&name) {\n                let slot = namespace_size;\n                namespace_size += 1;\n                cell_var_map.insert(name, NamespaceId::new(slot));\n            }\n        }\n\n        // Pre-populate free_var_map with nonlocal declarations AND implicit captures SECOND (after cell_vars).\n        // Each entry maps name -> namespace slot index where the cell reference will be stored.\n        // NOTE: We intentionally do NOT add these to name_map here, because the nonlocal\n        // validation in prepare_nodes checks name_map to detect \"used before nonlocal declaration\"\n        let free_var_capacity = nonlocal_names.len() + implicit_captures.len();\n        let mut free_var_map = AHashMap::with_capacity(free_var_capacity);\n        for name in nonlocal_names {\n            let slot = namespace_size;\n            namespace_size += 1;\n            free_var_map.insert(name, NamespaceId::new(slot));\n        }\n        // Implicit captures (variables accessed from enclosing scope without explicit nonlocal)\n        for name in implicit_captures {\n            let slot = namespace_size;\n            namespace_size += 1;\n            free_var_map.insert(name, NamespaceId::new(slot));\n        }\n\n        Self {\n            interner,\n            name_map,\n            namespace_size,\n            is_module_scope: false,\n            global_names,\n            assigned_names,\n            names_assigned_in_order: AHashSet::new(),\n            global_name_map: Some(global_name_map),\n            enclosing_locals,\n            free_var_map,\n            cell_var_map,\n            unassigned_ref_names: AHashSet::new(),\n        }\n    }\n\n    /// Recursively prepares a sequence of AST nodes by resolving names and transforming expressions.\n    ///\n    /// This method processes each node type differently:\n    /// - Resolves variable names to namespace indices\n    /// - Transforms function calls from identifier-based to builtin type-based\n    /// - Handles special cases like implicit returns in root frames\n    /// - Validates that names used in attribute calls are already defined\n    ///\n    /// # Returns\n    /// A vector of prepared nodes ready for compilation\n    fn prepare_nodes(&mut self, nodes: Vec<ParseNode>) -> Result<Vec<PreparedNode>, ParseError> {\n        let nodes_len = nodes.len();\n        let mut new_nodes = Vec::with_capacity(nodes_len);\n        for node in nodes {\n            match node {\n                Node::Pass => (),\n                Node::Expr(expr) => new_nodes.push(Node::Expr(self.prepare_expression(expr)?)),\n                Node::Return(expr) => new_nodes.push(Node::Return(self.prepare_expression(expr)?)),\n                Node::ReturnNone => new_nodes.push(Node::ReturnNone),\n                Node::Raise(exc) => {\n                    let expr = match exc {\n                        Some(expr) => {\n                            let prepared = self.prepare_expression(expr)?;\n                            match prepared.expr {\n                                // Handle raising a builtin exception type without instantiation,\n                                // e.g. `raise TypeError`. Transform into `raise TypeError()`\n                                // so the exception is properly instantiated before being raised.\n                                Expr::Builtin(b) => {\n                                    let call_expr = Expr::Call {\n                                        callable: Callable::Builtin(b),\n                                        args: Box::new(ArgExprs::Empty),\n                                    };\n                                    Some(ExprLoc::new(prepared.position, call_expr))\n                                }\n                                _ => Some(prepared),\n                            }\n                        }\n                        None => None,\n                    };\n                    new_nodes.push(Node::Raise(expr));\n                }\n                Node::Assert { test, msg } => {\n                    let test = self.prepare_expression(test)?;\n                    let msg = match msg {\n                        Some(m) => Some(self.prepare_expression(m)?),\n                        None => None,\n                    };\n                    new_nodes.push(Node::Assert { test, msg });\n                }\n                Node::Assign { target, object } => {\n                    let object = self.prepare_expression(object)?;\n                    // Track that this name was assigned before we call get_id\n                    self.names_assigned_in_order\n                        .insert(self.interner.get_str(target.name_id).to_string());\n                    let (target, _) = self.get_id(target);\n                    new_nodes.push(Node::Assign { target, object });\n                }\n                Node::UnpackAssign {\n                    targets,\n                    targets_position,\n                    object,\n                } => {\n                    let object = self.prepare_expression(object)?;\n                    // Recursively resolve all targets (supports nested tuples)\n                    let targets = targets\n                        .into_iter()\n                        .map(|target| self.prepare_unpack_target(target))\n                        .collect();\n                    new_nodes.push(Node::UnpackAssign {\n                        targets,\n                        targets_position,\n                        object,\n                    });\n                }\n                Node::OpAssign { target, op, object } => {\n                    // Track that this name was assigned\n                    self.names_assigned_in_order\n                        .insert(self.interner.get_str(target.name_id).to_string());\n                    let target = self.get_id(target).0;\n                    let object = self.prepare_expression(object)?;\n                    new_nodes.push(Node::OpAssign { target, op, object });\n                }\n                Node::SubscriptOpAssign {\n                    target,\n                    index,\n                    op,\n                    object,\n                    target_position,\n                } => {\n                    let target = self.get_id(target).0;\n                    let index = self.prepare_expression(index)?;\n                    let object = self.prepare_expression(object)?;\n                    new_nodes.push(Node::SubscriptOpAssign {\n                        target,\n                        index,\n                        op,\n                        object,\n                        target_position,\n                    });\n                }\n                Node::SubscriptAssign {\n                    target,\n                    index,\n                    value,\n                    target_position,\n                } => {\n                    // SubscriptAssign doesn't assign to the target itself, just modifies it\n                    let target = self.get_id(target).0;\n                    let index = self.prepare_expression(index)?;\n                    let value = self.prepare_expression(value)?;\n                    new_nodes.push(Node::SubscriptAssign {\n                        target,\n                        index,\n                        value,\n                        target_position,\n                    });\n                }\n                Node::AttrAssign {\n                    object,\n                    attr,\n                    target_position,\n                    value,\n                } => {\n                    // AttrAssign doesn't assign to the object itself, just modifies its attribute\n                    let object = self.prepare_expression(object)?;\n                    let value = self.prepare_expression(value)?;\n                    new_nodes.push(Node::AttrAssign {\n                        object,\n                        attr,\n                        target_position,\n                        value,\n                    });\n                }\n                Node::For {\n                    target,\n                    iter,\n                    body,\n                    or_else,\n                } => {\n                    // Prepare target with normal scoping (not comprehension isolation)\n                    let target = self.prepare_unpack_target(target);\n                    new_nodes.push(Node::For {\n                        target,\n                        iter: self.prepare_expression(iter)?,\n                        body: self.prepare_nodes(body)?,\n                        or_else: self.prepare_nodes(or_else)?,\n                    });\n                }\n                Node::Break { position } => {\n                    new_nodes.push(Node::Break { position });\n                }\n                Node::Continue { position } => {\n                    new_nodes.push(Node::Continue { position });\n                }\n                Node::While { test, body, or_else } => {\n                    new_nodes.push(Node::While {\n                        test: self.prepare_expression(test)?,\n                        body: self.prepare_nodes(body)?,\n                        or_else: self.prepare_nodes(or_else)?,\n                    });\n                }\n                Node::If { test, body, or_else } => {\n                    let test = self.prepare_expression(test)?;\n                    let body = self.prepare_nodes(body)?;\n                    let or_else = self.prepare_nodes(or_else)?;\n                    new_nodes.push(Node::If { test, body, or_else });\n                }\n                Node::FunctionDef(RawFunctionDef {\n                    name,\n                    signature,\n                    body,\n                    is_async,\n                }) => {\n                    let func_node = self.prepare_function_def(name, &signature, body, is_async)?;\n                    new_nodes.push(func_node);\n                }\n                Node::Global { names, position } => {\n                    // At module level, `global` is a no-op since all variables are already global.\n                    // In functions, the global declarations are already collected in the first pass\n                    // (see prepare_function_def), so this is also a no-op at this point.\n                    // The actual effect happens in get_id where we check global_names.\n                    if !self.is_module_scope {\n                        // Validate that names weren't already used/assigned before `global` declaration\n                        for string_id in names {\n                            let name_str = self.interner.get_str(string_id);\n                            if self.names_assigned_in_order.contains(name_str) {\n                                // Name was assigned before the global declaration\n                                return Err(ParseError::syntax(\n                                    format!(\"name '{name_str}' is assigned to before global declaration\"),\n                                    position,\n                                ));\n                            } else if self.name_map.contains_key(name_str) {\n                                // Name was used (but not assigned) before the global declaration\n                                return Err(ParseError::syntax(\n                                    format!(\"name '{name_str}' is used prior to global declaration\"),\n                                    position,\n                                ));\n                            }\n                        }\n                    }\n                    // Global statements don't produce any runtime nodes\n                }\n                Node::Nonlocal { names, position } => {\n                    // Nonlocal can only be used inside a function, not at module level\n                    if self.is_module_scope {\n                        return Err(ParseError::syntax(\n                            \"nonlocal declaration not allowed at module level\",\n                            position,\n                        ));\n                    }\n                    // Validate that names weren't already used/assigned before `nonlocal` declaration\n                    // and that the binding exists in an enclosing scope\n                    for string_id in names {\n                        let name_str = self.interner.get_str(string_id);\n                        if self.names_assigned_in_order.contains(name_str) {\n                            // Name was assigned before the nonlocal declaration\n                            return Err(ParseError::syntax(\n                                format!(\"name '{name_str}' is assigned to before nonlocal declaration\"),\n                                position,\n                            ));\n                        } else if self.name_map.contains_key(name_str) {\n                            // Name was used (but not assigned) before the nonlocal declaration\n                            return Err(ParseError::syntax(\n                                format!(\"name '{name_str}' is used prior to nonlocal declaration\"),\n                                position,\n                            ));\n                        }\n                        // Validate that the binding exists in an enclosing scope\n                        if let Some(ref enclosing) = self.enclosing_locals {\n                            if !enclosing.contains(name_str) {\n                                return Err(ParseError::syntax(\n                                    format!(\"no binding for nonlocal '{name_str}' found\"),\n                                    position,\n                                ));\n                            }\n                        } else {\n                            // No enclosing scope (function defined at module level)\n                            // The nonlocal must reference something in an enclosing function\n                            return Err(ParseError::syntax(\n                                format!(\"no binding for nonlocal '{name_str}' found\"),\n                                position,\n                            ));\n                        }\n                    }\n                    // Nonlocal statements don't produce any runtime nodes\n                }\n                Node::Try(Try {\n                    body,\n                    handlers,\n                    or_else,\n                    finally,\n                }) => {\n                    let body = self.prepare_nodes(body)?;\n                    let handlers = handlers\n                        .into_iter()\n                        .map(|h| self.prepare_except_handler(h))\n                        .collect::<Result<Vec<_>, _>>()?;\n                    let or_else = self.prepare_nodes(or_else)?;\n                    let finally = self.prepare_nodes(finally)?;\n                    new_nodes.push(Node::Try(Try {\n                        body,\n                        handlers,\n                        or_else,\n                        finally,\n                    }));\n                }\n                Node::Import { module_name, binding } => {\n                    // Resolve the binding identifier to get the namespace slot\n                    let (resolved_binding, _) = self.get_id(binding);\n                    new_nodes.push(Node::Import {\n                        module_name,\n                        binding: resolved_binding,\n                    });\n                }\n                Node::ImportFrom {\n                    module_name,\n                    names,\n                    position,\n                } => {\n                    // Resolve each binding identifier to get namespace slots\n                    let resolved_names = names\n                        .into_iter()\n                        .map(|(import_name, binding)| {\n                            let (resolved_binding, _) = self.get_id(binding);\n                            (import_name, resolved_binding)\n                        })\n                        .collect();\n                    new_nodes.push(Node::ImportFrom {\n                        module_name,\n                        names: resolved_names,\n                        position,\n                    });\n                }\n            }\n        }\n        Ok(new_nodes)\n    }\n\n    /// Prepares an exception handler by resolving names in the exception type and body.\n    ///\n    /// The exception variable (if present) is treated as an assigned name in the current scope.\n    fn prepare_except_handler(\n        &mut self,\n        handler: ExceptHandler<ParseNode>,\n    ) -> Result<ExceptHandler<PreparedNode>, ParseError> {\n        let exc_type = match handler.exc_type {\n            Some(expr) => Some(self.prepare_expression(expr)?),\n            None => None,\n        };\n        // The exception variable binding (e.g., `as e:`) is an assignment\n        let name = match handler.name {\n            Some(ident) => {\n                // Track that this name was assigned\n                self.names_assigned_in_order\n                    .insert(self.interner.get_str(ident.name_id).to_string());\n                Some(self.get_id(ident).0)\n            }\n            None => None,\n        };\n        let body = self.prepare_nodes(handler.body)?;\n        Ok(ExceptHandler { exc_type, name, body })\n    }\n\n    /// Prepares an expression by resolving names, transforming calls, and applying optimizations.\n    ///\n    /// Key transformations performed:\n    /// - Name lookups are resolved to namespace indices via `get_id`\n    /// - Function calls are resolved from identifiers to builtin types\n    /// - Attribute calls validate that the object is already defined (not a new name)\n    /// - Lists and tuples are recursively prepared\n    /// - Modulo equality patterns like `x % n == k` (constant right-hand side) are optimized to\n    ///   `CmpOperator::ModEq`\n    ///\n    /// # Errors\n    /// Returns a NameError if an attribute call references an undefined variable\n    fn prepare_expression(&mut self, loc_expr: ExprLoc) -> Result<ExprLoc, ParseError> {\n        let ExprLoc { position, expr } = loc_expr;\n        let expr = match expr {\n            Expr::Literal(object) => Expr::Literal(object),\n            Expr::Builtin(callable) => Expr::Builtin(callable),\n            Expr::Name(name) => self.resolve_name_or_builtin(name),\n            Expr::Op { left, op, right } => Expr::Op {\n                left: Box::new(self.prepare_expression(*left)?),\n                op,\n                right: Box::new(self.prepare_expression(*right)?),\n            },\n            Expr::CmpOp { left, op, right } => Expr::CmpOp {\n                left: Box::new(self.prepare_expression(*left)?),\n                op,\n                right: Box::new(self.prepare_expression(*right)?),\n            },\n            Expr::ChainCmp { left, comparisons } => Expr::ChainCmp {\n                left: Box::new(self.prepare_expression(*left)?),\n                comparisons: comparisons\n                    .into_iter()\n                    .map(|(op, expr)| Ok((op, self.prepare_expression(expr)?)))\n                    .collect::<Result<Vec<_>, _>>()?,\n            },\n            Expr::Call { callable, mut args } => {\n                // Prepare the arguments\n                args.prepare_args(|expr| self.prepare_expression(expr))?;\n                // For Name callables, resolve the identifier in the namespace\n                // Don't error here if undefined - let runtime raise NameError with proper traceback\n                let callable = match callable {\n                    Callable::Name(ident) => match self.resolve_name_or_builtin(ident) {\n                        Expr::Builtin(b) => Callable::Builtin(b),\n                        Expr::Name(resolved) => Callable::Name(resolved),\n                        _ => unreachable!(\"resolve_name_or_builtin returns Name or Builtin\"),\n                    },\n                    other @ Callable::Builtin(_) => other,\n                };\n                Expr::Call { callable, args }\n            }\n            Expr::AttrCall { object, attr, mut args } => {\n                // Prepare the object expression (supports chained access like a.b.c.method())\n                let object = Box::new(self.prepare_expression(*object)?);\n                args.prepare_args(|expr| self.prepare_expression(expr))?;\n                Expr::AttrCall { object, attr, args }\n            }\n            Expr::IndirectCall { callable, mut args } => {\n                // Prepare the callable expression (e.g., lambda or any expression returning a callable)\n                let callable = Box::new(self.prepare_expression(*callable)?);\n                args.prepare_args(|expr| self.prepare_expression(expr))?;\n                Expr::IndirectCall { callable, args }\n            }\n            Expr::AttrGet { object, attr } => {\n                // Prepare the object expression (supports chained access like a.b.c)\n                let object = Box::new(self.prepare_expression(*object)?);\n                Expr::AttrGet { object, attr }\n            }\n            Expr::List(elements) => {\n                let items = elements\n                    .into_iter()\n                    .map(|item| self.prepare_sequence_item(item))\n                    .collect::<Result<_, ParseError>>()?;\n                Expr::List(items)\n            }\n            Expr::Tuple(elements) => {\n                let items = elements\n                    .into_iter()\n                    .map(|item| self.prepare_sequence_item(item))\n                    .collect::<Result<_, ParseError>>()?;\n                Expr::Tuple(items)\n            }\n            Expr::Subscript { object, index } => Expr::Subscript {\n                object: Box::new(self.prepare_expression(*object)?),\n                index: Box::new(self.prepare_expression(*index)?),\n            },\n            Expr::Dict(dict_items) => {\n                let prepared = dict_items\n                    .into_iter()\n                    .map(|item| match item {\n                        DictItem::Pair(k, v) => {\n                            Ok(DictItem::Pair(self.prepare_expression(k)?, self.prepare_expression(v)?))\n                        }\n                        DictItem::Unpack(e) => Ok(DictItem::Unpack(self.prepare_expression(e)?)),\n                    })\n                    .collect::<Result<_, ParseError>>()?;\n                Expr::Dict(prepared)\n            }\n            Expr::Set(elements) => {\n                let items = elements\n                    .into_iter()\n                    .map(|item| self.prepare_sequence_item(item))\n                    .collect::<Result<_, ParseError>>()?;\n                Expr::Set(items)\n            }\n            Expr::Not(operand) => Expr::Not(Box::new(self.prepare_expression(*operand)?)),\n            Expr::UnaryMinus(operand) => Expr::UnaryMinus(Box::new(self.prepare_expression(*operand)?)),\n            Expr::UnaryPlus(operand) => Expr::UnaryPlus(Box::new(self.prepare_expression(*operand)?)),\n            Expr::UnaryInvert(operand) => Expr::UnaryInvert(Box::new(self.prepare_expression(*operand)?)),\n            Expr::FString(parts) => {\n                let prepared_parts = parts\n                    .into_iter()\n                    .map(|part| self.prepare_fstring_part(part))\n                    .collect::<Result<Vec<_>, ParseError>>()?;\n                Expr::FString(prepared_parts)\n            }\n            Expr::IfElse { test, body, orelse } => Expr::IfElse {\n                test: Box::new(self.prepare_expression(*test)?),\n                body: Box::new(self.prepare_expression(*body)?),\n                orelse: Box::new(self.prepare_expression(*orelse)?),\n            },\n            Expr::ListComp { elt, generators } => {\n                let (generators, elt, _) = self.prepare_comprehension(generators, Some(*elt), None)?;\n                Expr::ListComp {\n                    elt: Box::new(elt.expect(\"list comp must have elt\")),\n                    generators,\n                }\n            }\n            Expr::SetComp { elt, generators } => {\n                let (generators, elt, _) = self.prepare_comprehension(generators, Some(*elt), None)?;\n                Expr::SetComp {\n                    elt: Box::new(elt.expect(\"set comp must have elt\")),\n                    generators,\n                }\n            }\n            Expr::DictComp { key, value, generators } => {\n                let (generators, _, key_value) = self.prepare_comprehension(generators, None, Some((*key, *value)))?;\n                let (key, value) = key_value.expect(\"dict comp must have key/value\");\n                Expr::DictComp {\n                    key: Box::new(key),\n                    value: Box::new(value),\n                    generators,\n                }\n            }\n            Expr::LambdaRaw {\n                name_id,\n                signature,\n                body,\n            } => {\n                // Convert the raw lambda into a prepared lambda expression\n                return self.prepare_lambda(name_id, &signature, &body, position);\n            }\n            Expr::Lambda { .. } => {\n                // Lambda should only be created during prepare, never during parsing\n                unreachable!(\"Expr::Lambda should not exist before prepare phase\")\n            }\n            Expr::Slice { lower, upper, step } => Expr::Slice {\n                lower: lower.map(|e| self.prepare_expression(*e)).transpose()?.map(Box::new),\n                upper: upper.map(|e| self.prepare_expression(*e)).transpose()?.map(Box::new),\n                step: step.map(|e| self.prepare_expression(*e)).transpose()?.map(Box::new),\n            },\n            Expr::Named { target, value } => {\n                let value = Box::new(self.prepare_expression(*value)?);\n                // Register the target as assigned in this scope\n                self.names_assigned_in_order\n                    .insert(self.interner.get_str(target.name_id).to_string());\n                let (resolved_target, _) = self.get_id(target);\n                Expr::Named {\n                    target: resolved_target,\n                    value,\n                }\n            }\n            Expr::Await(value) => Expr::Await(Box::new(self.prepare_expression(*value)?)),\n        };\n\n        // Optimization: Transform `(x % n) == value` with any constant right-hand side into a\n        // specialized ModEq operator.\n        // This is a common pattern in competitive programming (e.g., FizzBuzz checks like `i % 3 == 0`)\n        // and can be executed more efficiently with a single modulo operation + comparison\n        // instead of separate modulo, then equality check.\n        if let Expr::CmpOp { left, op, right } = &expr\n            && op == &CmpOperator::Eq\n            && let Expr::Literal(Literal::Int(value)) = right.expr\n            && let Expr::Op {\n                left: left2,\n                op,\n                right: right2,\n            } = &left.expr\n            && op == &Operator::Mod\n        {\n            let new_expr = Expr::CmpOp {\n                left: left2.clone(),\n                op: CmpOperator::ModEq(value),\n                right: right2.clone(),\n            };\n            return Ok(ExprLoc {\n                position: left.position,\n                expr: new_expr,\n            });\n        }\n\n        Ok(ExprLoc { position, expr })\n    }\n\n    /// Resolves a name to either `Expr::Builtin` or `Expr::Name` with scope-aware builtin detection.\n    ///\n    /// Python's name resolution follows LEGB order (Local, Enclosing, Global, Builtin).\n    /// Builtins are only used when the name is not found in any other scope. This method\n    /// ensures that local assignments (e.g., `int = 42`) properly shadow builtin names.\n    ///\n    /// We check before calling `get_id` to avoid allocating unnecessary namespace slots.\n    /// At module level, a slot allocated for an unassigned builtin would leak into\n    /// `global_name_map` for nested functions, causing incorrect resolution.\n    fn resolve_name_or_builtin(&mut self, name: Identifier) -> Expr {\n        let name_str = self.interner.get_str(name.name_id);\n\n        // Check if the name is assigned in the current scope. If so, it shadows\n        // any builtin with the same name.\n        let is_locally_assigned = if self.is_module_scope {\n            // Module scope: sequential — only names assigned SO FAR shadow builtins\n            self.names_assigned_in_order.contains(name_str)\n        } else {\n            // Function scope: lexical — ANY assignment in the function body makes\n            // the name local for the entire function\n            self.assigned_names.contains(name_str)\n        };\n\n        if !is_locally_assigned {\n            // In function scope, also check if the name is bound by other mechanisms\n            // (global declaration, parameter, closure capture, enclosing/global scope).\n            // Only fall back to builtins if the name is truly unresolved.\n            let is_otherwise_bound = !self.is_module_scope\n                && (self.global_names.contains(name_str)\n                    || self.free_var_map.contains_key(name_str)\n                    || self.cell_var_map.contains_key(name_str)\n                    || self.name_map.contains_key(name_str)\n                    || self.enclosing_locals.as_ref().is_some_and(|l| l.contains(name_str))\n                    || self.global_name_map.as_ref().is_some_and(|m| m.contains_key(name_str)));\n\n            if !is_otherwise_bound && let Ok(builtin) = name_str.parse::<Builtins>() {\n                return Expr::Builtin(builtin);\n            }\n        }\n\n        Expr::Name(self.get_id(name).0)\n    }\n\n    /// Prepares a `SequenceItem` by recursively preparing its inner expression.\n    ///\n    /// Both `Value` and `Unpack` variants need their expressions prepared\n    /// (name resolution, scope analysis, builtin detection, etc.).\n    fn prepare_sequence_item(&mut self, item: SequenceItem) -> Result<SequenceItem, ParseError> {\n        match item {\n            SequenceItem::Value(e) => Ok(SequenceItem::Value(self.prepare_expression(e)?)),\n            SequenceItem::Unpack(e) => Ok(SequenceItem::Unpack(self.prepare_expression(e)?)),\n        }\n    }\n\n    /// Prepares a comprehension with scope isolation for loop variables.\n    ///\n    /// Comprehension loop variables are isolated from the enclosing scope - they do not\n    /// leak after the comprehension completes. CPython scoping rules require:\n    ///\n    /// 1. The FIRST generator's iter is evaluated in the enclosing scope\n    /// 2. ALL loop variables from ALL generators are then shadowed as local\n    /// 3. Subsequent generators' iters see all loop vars as local (even if unassigned)\n    ///\n    /// This means `[y for x in [1] for y in z for z in [[2]]]` raises UnboundLocalError\n    /// because `z` is treated as local (it's a loop var in generator 3) when evaluating\n    /// generator 2's iter.\n    ///\n    /// For list/set comprehensions, pass `elt` as Some and `key_value` as None.\n    /// For dict comprehensions, pass `elt` as None and `key_value` as Some((key, value)).\n    #[expect(clippy::type_complexity)]\n    fn prepare_comprehension(\n        &mut self,\n        generators: Vec<Comprehension>,\n        elt: Option<ExprLoc>,\n        key_value: Option<(ExprLoc, ExprLoc)>,\n    ) -> Result<(Vec<Comprehension>, Option<ExprLoc>, Option<(ExprLoc, ExprLoc)>), ParseError> {\n        // Per PEP 572, walrus operators inside comprehensions bind in the ENCLOSING scope.\n        // Pre-register walrus targets before saving scope state, so they persist after restore.\n        let mut walrus_targets: AHashSet<String> = AHashSet::new();\n        if let Some(ref e) = elt {\n            collect_assigned_names_from_expr(e, &mut walrus_targets, self.interner);\n        }\n        if let Some((ref k, ref v)) = key_value {\n            collect_assigned_names_from_expr(k, &mut walrus_targets, self.interner);\n            collect_assigned_names_from_expr(v, &mut walrus_targets, self.interner);\n        }\n        for generator in &generators {\n            // Note: we don't scan iter expressions here because walrus in iterable is not allowed\n            for cond in &generator.ifs {\n                collect_assigned_names_from_expr(cond, &mut walrus_targets, self.interner);\n            }\n        }\n        // Pre-allocate slots for walrus targets in the enclosing scope\n        for name in &walrus_targets {\n            if !self.name_map.contains_key(name) {\n                let slot = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n                self.name_map.insert(name.clone(), slot);\n                self.names_assigned_in_order.insert(name.clone());\n            }\n        }\n\n        // Save current scope state for isolation\n        let saved_name_map = self.name_map.clone();\n        let saved_assigned_names = self.names_assigned_in_order.clone();\n        let saved_free_var_map = self.free_var_map.clone();\n        let saved_cell_var_map = self.cell_var_map.clone();\n        let saved_enclosing_locals = self.enclosing_locals.clone();\n        let saved_unassigned_ref_names = self.unassigned_ref_names.clone();\n\n        // Step 1: Prepare first generator's iter in enclosing scope (before any shadowing)\n        let mut generators_iter = generators.into_iter();\n        let first_gen = generators_iter\n            .next()\n            .expect(\"comprehension must have at least one generator\");\n        let first_iter = self.prepare_expression(first_gen.iter)?;\n\n        // Step 2: Collect and shadow ALL loop variable names from ALL generators.\n        // This must happen BEFORE evaluating any subsequent generator's iter expression.\n        // We allocate slots but don't mark them as \"assigned\" yet - this causes\n        // UnboundLocalError if a later generator's iter references an earlier-declared\n        // but not-yet-assigned loop variable.\n        let first_target = self.prepare_unpack_target_for_comprehension(first_gen.target);\n\n        // Collect remaining generators so we can pre-shadow their targets\n        let remaining_gens: Vec<Comprehension> = generators_iter.collect();\n\n        // Pre-shadow ALL remaining loop variables before evaluating their iters.\n        // This is the key CPython behavior: all loop vars are local to the comprehension,\n        // so referencing a later loop var in an earlier iter raises UnboundLocalError.\n        let mut preshadowed_targets: Vec<UnpackTarget> = Vec::with_capacity(remaining_gens.len());\n        for generator in &remaining_gens {\n            preshadowed_targets.push(self.prepare_unpack_target_shadow_only(generator.target.clone()));\n        }\n\n        // Prepare first generator's filters (can see first loop variable)\n        let first_ifs = first_gen\n            .ifs\n            .into_iter()\n            .map(|cond| self.prepare_expression(cond))\n            .collect::<Result<Vec<_>, _>>()?;\n\n        let mut prepared_generators = Vec::with_capacity(1 + remaining_gens.len());\n        prepared_generators.push(Comprehension {\n            target: first_target,\n            iter: first_iter,\n            ifs: first_ifs,\n        });\n\n        // Step 3: Process remaining generators - their iters now see all loop vars as local\n        for (generator, preshadowed_target) in remaining_gens.into_iter().zip(preshadowed_targets) {\n            let iter = self.prepare_expression(generator.iter)?;\n            let ifs = generator\n                .ifs\n                .into_iter()\n                .map(|cond| self.prepare_expression(cond))\n                .collect::<Result<Vec<_>, _>>()?;\n\n            prepared_generators.push(Comprehension {\n                target: preshadowed_target,\n                iter,\n                ifs,\n            });\n        }\n\n        // Prepare the element expression(s) - can see all loop variables\n        let prepared_elt = match elt {\n            Some(e) => Some(self.prepare_expression(e)?),\n            None => None,\n        };\n        let prepared_key_value = match key_value {\n            Some((k, v)) => Some((self.prepare_expression(k)?, self.prepare_expression(v)?)),\n            None => None,\n        };\n\n        // Restore scope state - loop variables do not leak to enclosing scope\n        self.name_map = saved_name_map;\n        self.names_assigned_in_order = saved_assigned_names;\n        self.free_var_map = saved_free_var_map;\n        self.cell_var_map = saved_cell_var_map;\n        self.enclosing_locals = saved_enclosing_locals;\n        self.unassigned_ref_names = saved_unassigned_ref_names;\n\n        Ok((prepared_generators, prepared_elt, prepared_key_value))\n    }\n\n    /// Prepares an unpack target by resolving identifiers recursively.\n    ///\n    /// Handles both single identifiers and nested tuples like `(a, b), c`.\n    fn prepare_unpack_target(&mut self, target: UnpackTarget) -> UnpackTarget {\n        match target {\n            UnpackTarget::Name(ident) => {\n                self.names_assigned_in_order\n                    .insert(self.interner.get_str(ident.name_id).to_string());\n                UnpackTarget::Name(self.get_id(ident).0)\n            }\n            UnpackTarget::Starred(ident) => {\n                self.names_assigned_in_order\n                    .insert(self.interner.get_str(ident.name_id).to_string());\n                UnpackTarget::Starred(self.get_id(ident).0)\n            }\n            UnpackTarget::Tuple { targets, position } => {\n                let resolved_targets: Vec<UnpackTarget> = targets\n                    .into_iter()\n                    .map(|t| self.prepare_unpack_target(t)) // Recursive call\n                    .collect();\n                UnpackTarget::Tuple {\n                    targets: resolved_targets,\n                    position,\n                }\n            }\n        }\n    }\n\n    /// Prepares an unpack target for comprehension by allocating fresh namespace slots.\n    ///\n    /// Unlike regular unpack targets, comprehension targets need new slots to shadow\n    /// any existing bindings with the same name.\n    fn prepare_unpack_target_for_comprehension(&mut self, target: UnpackTarget) -> UnpackTarget {\n        match target {\n            UnpackTarget::Name(ident) => {\n                let name_str = self.interner.get_str(ident.name_id).to_string();\n                let comp_var_id = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n\n                // Shadow any existing binding\n                self.shadow_for_comprehension(&name_str, comp_var_id);\n\n                UnpackTarget::Name(Identifier::new_with_scope(\n                    ident.name_id,\n                    ident.position,\n                    comp_var_id,\n                    NameScope::Local,\n                ))\n            }\n            UnpackTarget::Starred(ident) => {\n                let name_str = self.interner.get_str(ident.name_id).to_string();\n                let comp_var_id = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n\n                // Shadow any existing binding\n                self.shadow_for_comprehension(&name_str, comp_var_id);\n\n                UnpackTarget::Starred(Identifier::new_with_scope(\n                    ident.name_id,\n                    ident.position,\n                    comp_var_id,\n                    NameScope::Local,\n                ))\n            }\n            UnpackTarget::Tuple { targets, position } => {\n                let resolved_targets: Vec<UnpackTarget> = targets\n                    .into_iter()\n                    .map(|t| self.prepare_unpack_target_for_comprehension(t)) // Recursive call\n                    .collect();\n                UnpackTarget::Tuple {\n                    targets: resolved_targets,\n                    position,\n                }\n            }\n        }\n    }\n\n    /// Pre-shadows an unpack target for comprehension scoping.\n    ///\n    /// Allocates namespace slots without marking as assigned, causing UnboundLocalError\n    /// if accessed before assignment.\n    fn prepare_unpack_target_shadow_only(&mut self, target: UnpackTarget) -> UnpackTarget {\n        match target {\n            UnpackTarget::Name(ident) => {\n                let name_str = self.interner.get_str(ident.name_id).to_string();\n                let comp_var_id = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n\n                // Shadow but do NOT add to names_assigned_in_order yet\n                self.name_map.insert(name_str.clone(), comp_var_id);\n                self.free_var_map.remove(&name_str);\n                self.cell_var_map.remove(&name_str);\n                if let Some(ref mut enclosing) = self.enclosing_locals {\n                    enclosing.remove(&name_str);\n                }\n\n                UnpackTarget::Name(Identifier::new_with_scope(\n                    ident.name_id,\n                    ident.position,\n                    comp_var_id,\n                    NameScope::Local,\n                ))\n            }\n            UnpackTarget::Starred(ident) => {\n                let name_str = self.interner.get_str(ident.name_id).to_string();\n                let comp_var_id = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n\n                // Shadow but do NOT add to names_assigned_in_order yet\n                self.name_map.insert(name_str.clone(), comp_var_id);\n                self.free_var_map.remove(&name_str);\n                self.cell_var_map.remove(&name_str);\n                if let Some(ref mut enclosing) = self.enclosing_locals {\n                    enclosing.remove(&name_str);\n                }\n\n                UnpackTarget::Starred(Identifier::new_with_scope(\n                    ident.name_id,\n                    ident.position,\n                    comp_var_id,\n                    NameScope::Local,\n                ))\n            }\n            UnpackTarget::Tuple { targets, position } => {\n                let resolved_targets: Vec<UnpackTarget> = targets\n                    .into_iter()\n                    .map(|t| self.prepare_unpack_target_shadow_only(t)) // Recursive call\n                    .collect();\n                UnpackTarget::Tuple {\n                    targets: resolved_targets,\n                    position,\n                }\n            }\n        }\n    }\n\n    /// Shadows a name in all scope maps for comprehension isolation.\n    ///\n    /// This ensures the comprehension loop variable takes precedence over any\n    /// variable with the same name from enclosing scopes.\n    fn shadow_for_comprehension(&mut self, name_str: &str, comp_var_id: NamespaceId) {\n        // The lookup order in get_id is: global_declarations, free_var_map, cell_var_map,\n        // assigned_names, enclosing_locals, then name_map. So we must update/remove from all maps\n        // checked before name_map to ensure the comprehension variable shadows any captured\n        // variable with the same name.\n        self.name_map.insert(name_str.to_string(), comp_var_id);\n        self.names_assigned_in_order.insert(name_str.to_string());\n        self.free_var_map.remove(name_str);\n        self.cell_var_map.remove(name_str);\n        // Also remove from enclosing_locals to prevent get_id from re-capturing the variable\n        if let Some(ref mut enclosing) = self.enclosing_locals {\n            enclosing.remove(name_str);\n        }\n    }\n\n    /// Prepares a function definition using a two-pass approach for correct scope resolution.\n    ///\n    /// Pass 1: Scan the function body to collect:\n    /// - Names declared as `global`\n    /// - Names declared as `nonlocal`\n    /// - Names that are assigned (these are local unless declared global/nonlocal)\n    ///\n    /// Pass 2: Prepare the function body with the scope information from pass 1.\n    ///\n    /// # Closure Analysis\n    ///\n    /// When the nested function uses `nonlocal` declarations, those names must exist\n    /// in an enclosing scope. The enclosing scope's variable becomes a cell_var\n    /// (stored in a heap cell), and the nested function captures it as a free_var.\n    fn prepare_function_def(\n        &mut self,\n        name: Identifier,\n        parsed_sig: &ParsedSignature,\n        body: Vec<ParseNode>,\n        is_async: bool,\n    ) -> Result<PreparedNode, ParseError> {\n        // Register the function name in the current scope\n        let (name, _) = self.get_id(name);\n\n        // Extract param names from the parsed signature for scope analysis\n        let param_names: Vec<StringId> = parsed_sig.param_names().collect();\n\n        // Pass 1: Collect scope information from the function body\n        let scope_info = collect_function_scope_info(&body, &param_names, self.interner);\n\n        // Get the global name map to pass to the function preparer\n        // At module level, use our own name_map; otherwise use the inherited global_name_map\n        let global_name_map = if self.is_module_scope {\n            self.name_map.clone()\n        } else {\n            self.global_name_map.clone().unwrap_or_default()\n        };\n\n        // Build enclosing_locals: names that are local to this scope (including params)\n        // These are available for `nonlocal` declarations in nested functions\n        let enclosing_locals: AHashSet<String> = if self.is_module_scope {\n            // At module level, there are no enclosing locals for nonlocal\n            // (module-level variables are accessed via `global`, not `nonlocal`)\n            AHashSet::new()\n        } else {\n            // In a function: our params + assigned_names + existing name_map keys\n            // are all potentially available as enclosing locals\n            let mut locals = self.assigned_names.clone();\n            for key in self.name_map.keys() {\n                locals.insert(key.clone());\n            }\n            locals\n        };\n\n        // Filter potential_captures to get actual implicit captures.\n        // Only names that are ALSO in enclosing_locals are true implicit captures.\n        // Names NOT in enclosing_locals are either builtins or globals (handled at runtime).\n        let implicit_captures: AHashSet<String> = scope_info\n            .potential_captures\n            .into_iter()\n            .filter(|name| enclosing_locals.contains(name))\n            .collect();\n\n        // Pass 2: Create child preparer for function body with scope info\n        let mut inner_prepare = Prepare::new_function(\n            body.len(),\n            &param_names,\n            scope_info.assigned_names,\n            scope_info.global_names,\n            scope_info.nonlocal_names,\n            implicit_captures,\n            global_name_map,\n            Some(enclosing_locals),\n            scope_info.cell_var_names,\n            self.interner,\n        );\n\n        // Prepare the function body\n        let prepared_body = inner_prepare.prepare_nodes(body)?;\n\n        // Mark variables that the inner function captures as our cell_vars\n        // These are the names that appear in inner_prepare.free_var_map\n        // Add to cell_var_map if not already present (may have been pre-populated or added earlier)\n        for captured_name in inner_prepare.free_var_map.keys() {\n            if !self.cell_var_map.contains_key(captured_name) && !self.free_var_map.contains_key(captured_name) {\n                // Only add to cell_var_map if not already a free_var (pass-through case)\n                // Allocate a namespace slot for the cell reference\n                let slot = match self.name_map.entry(captured_name.clone()) {\n                    Entry::Occupied(e) => *e.get(),\n                    Entry::Vacant(e) => {\n                        let slot = NamespaceId::new(self.namespace_size);\n                        self.namespace_size += 1;\n                        e.insert(slot);\n                        slot\n                    }\n                };\n                self.cell_var_map.insert(captured_name.clone(), slot);\n            }\n        }\n\n        // Build free_var_enclosing_slots: enclosing namespace slots for captured variables\n        // At call time, cells are pushed sequentially, so we only need the enclosing slots.\n        // Sort by our slot index to ensure consistent ordering (matches namespace layout).\n        let mut free_var_entries: Vec<_> = inner_prepare.free_var_map.into_iter().collect();\n        free_var_entries.sort_by_key(|(_, our_slot)| *our_slot);\n\n        let free_var_enclosing_slots: Vec<NamespaceId> = free_var_entries\n            .into_iter()\n            .map(|(var_name, _our_slot)| {\n                // Determine the namespace slot in the enclosing scope where the cell reference lives:\n                // - If it's in cell_var_map, it's a cell we own (allocated in this scope)\n                // - If it's in free_var_map, it's a cell we captured from further up\n                // - Otherwise, this is a prepare-time bug\n                if let Some(&slot) = self.cell_var_map.get(&var_name) {\n                    slot\n                } else if let Some(&slot) = self.free_var_map.get(&var_name) {\n                    slot\n                } else {\n                    panic!(\"free_var '{var_name}' not found in enclosing scope's cell_var_map or free_var_map\");\n                }\n            })\n            .collect();\n\n        // cell_var_count: number of cells to create at call time for variables captured by nested functions\n        // Slots are implicitly params.len()..params.len()+cell_var_count in the namespace layout\n        let cell_var_count = inner_prepare.cell_var_map.len();\n        let namespace_size = inner_prepare.namespace_size;\n\n        // Build cell_param_indices: maps cell indices to parameter indices for captured parameters.\n        // When a parameter is captured by a nested function, we need to copy its value into the cell.\n        let cell_param_indices: Vec<Option<usize>> = if cell_var_count == 0 {\n            Vec::new()\n        } else {\n            // Build a map from param name (String) to param index\n            let param_name_to_index: AHashMap<String, usize> = param_names\n                .iter()\n                .enumerate()\n                .map(|(idx, &name_id)| (self.interner.get_str(name_id).to_string(), idx))\n                .collect();\n\n            // Sort cell_var_map entries by slot to get cells in order\n            let mut cell_entries: Vec<_> = inner_prepare.cell_var_map.iter().collect();\n            cell_entries.sort_by_key(|&(_, slot)| slot);\n\n            // For each cell (in slot order), check if it's a parameter\n            cell_entries\n                .into_iter()\n                .map(|(name, _slot)| param_name_to_index.get(name).copied())\n                .collect()\n        };\n\n        // Build the runtime Signature from the parsed signature\n        let pos_args: Vec<StringId> = parsed_sig.pos_args.iter().map(|p| p.name).collect();\n        let pos_defaults_count = parsed_sig.pos_args.iter().filter(|p| p.default.is_some()).count();\n        let args: Vec<StringId> = parsed_sig.args.iter().map(|p| p.name).collect();\n        let arg_defaults_count = parsed_sig.args.iter().filter(|p| p.default.is_some()).count();\n        let mut kwargs: Vec<StringId> = Vec::with_capacity(parsed_sig.kwargs.len());\n        let mut kwarg_default_map: Vec<Option<usize>> = Vec::with_capacity(parsed_sig.kwargs.len());\n        let mut kwarg_default_index = 0;\n        for param in &parsed_sig.kwargs {\n            kwargs.push(param.name);\n            if param.default.is_some() {\n                kwarg_default_map.push(Some(kwarg_default_index));\n                kwarg_default_index += 1;\n            } else {\n                kwarg_default_map.push(None);\n            }\n        }\n\n        let signature = Signature::new(\n            pos_args,\n            pos_defaults_count,\n            args,\n            arg_defaults_count,\n            parsed_sig.var_args,\n            kwargs,\n            kwarg_default_map,\n            parsed_sig.var_kwargs,\n        );\n\n        // Collect and prepare default expressions in order: pos_args -> args -> kwargs\n        // Only includes parameters that actually have defaults.\n        let mut default_exprs = Vec::with_capacity(signature.total_defaults_count());\n        for param in &parsed_sig.pos_args {\n            if let Some(ref expr) = param.default {\n                default_exprs.push(self.prepare_expression(expr.clone())?);\n            }\n        }\n        for param in &parsed_sig.args {\n            if let Some(ref expr) = param.default {\n                default_exprs.push(self.prepare_expression(expr.clone())?);\n            }\n        }\n        for param in &parsed_sig.kwargs {\n            if let Some(ref expr) = param.default {\n                default_exprs.push(self.prepare_expression(expr.clone())?);\n            }\n        }\n\n        // Return the prepared function definition inline in the AST\n        Ok(Node::FunctionDef(PreparedFunctionDef {\n            name,\n            signature,\n            body: prepared_body,\n            namespace_size,\n            free_var_enclosing_slots,\n            cell_var_count,\n            cell_param_indices,\n            default_exprs,\n            is_async,\n        }))\n    }\n\n    /// Prepares a lambda expression, converting it into a prepared function definition.\n    ///\n    /// Lambdas are essentially anonymous functions with an implicit return of their body\n    /// expression. This method follows the same preparation logic as `prepare_function_def`\n    /// but:\n    /// - Uses `<lambda>` as the function name (not registered in scope)\n    /// - Wraps the body expression as `Node::Return(body)`\n    /// - Returns `ExprLoc` with `Expr::Lambda` instead of `PreparedNode`\n    fn prepare_lambda(\n        &mut self,\n        lambda_name_id: StringId,\n        parsed_sig: &ParsedSignature,\n        body: &ExprLoc,\n        position: CodeRange,\n    ) -> Result<ExprLoc, ParseError> {\n        // Create a synthetic <lambda> name identifier (not registered in scope)\n        let lambda_name = Identifier::new_with_scope(\n            lambda_name_id,\n            position,\n            NamespaceId::new(0), // Placeholder, not actually used for storage\n            NameScope::Local,\n        );\n\n        // Wrap the body expression as a return statement for scope analysis\n        let body_as_node: ParseNode = Node::Return(body.clone());\n        let body_nodes = vec![body_as_node];\n\n        // Extract param names from the parsed signature for scope analysis\n        let param_names: Vec<StringId> = parsed_sig.param_names().collect();\n\n        // Pass 1: Collect scope information from the lambda body\n        // (Lambdas can't have global/nonlocal declarations, but can have nested functions)\n        let scope_info = collect_function_scope_info(&body_nodes, &param_names, self.interner);\n\n        // Get the global name map to pass to the function preparer\n        let global_name_map = if self.is_module_scope {\n            self.name_map.clone()\n        } else {\n            self.global_name_map.clone().unwrap_or_default()\n        };\n\n        // Build enclosing_locals: names that are local to this scope or captured from enclosing scope.\n        // This includes free_vars so that nested lambdas can capture pass-through variables.\n        let enclosing_locals: AHashSet<String> = if self.is_module_scope {\n            AHashSet::new()\n        } else {\n            let mut locals = self.assigned_names.clone();\n            for key in self.name_map.keys() {\n                locals.insert(key.clone());\n            }\n            // Include free_vars so nested functions/lambdas can capture pass-through variables\n            for key in self.free_var_map.keys() {\n                locals.insert(key.clone());\n            }\n            locals\n        };\n\n        // Filter potential_captures to get actual implicit captures\n        let implicit_captures: AHashSet<String> = scope_info\n            .potential_captures\n            .into_iter()\n            .filter(|name| enclosing_locals.contains(name))\n            .collect();\n\n        // Pass 2: Create child preparer for lambda body with scope info\n        let mut inner_prepare = Prepare::new_function(\n            body_nodes.len(),\n            &param_names,\n            scope_info.assigned_names,\n            scope_info.global_names,\n            scope_info.nonlocal_names,\n            implicit_captures,\n            global_name_map,\n            Some(enclosing_locals),\n            scope_info.cell_var_names,\n            self.interner,\n        );\n\n        // Prepare the lambda body\n        let prepared_body = inner_prepare.prepare_nodes(body_nodes)?;\n\n        // Mark variables that the inner function captures as our cell_vars\n        for captured_name in inner_prepare.free_var_map.keys() {\n            if !self.cell_var_map.contains_key(captured_name) && !self.free_var_map.contains_key(captured_name) {\n                let slot = match self.name_map.entry(captured_name.clone()) {\n                    Entry::Occupied(e) => *e.get(),\n                    Entry::Vacant(e) => {\n                        let slot = NamespaceId::new(self.namespace_size);\n                        self.namespace_size += 1;\n                        e.insert(slot);\n                        slot\n                    }\n                };\n                self.cell_var_map.insert(captured_name.clone(), slot);\n            }\n        }\n\n        // Build free_var_enclosing_slots\n        let mut free_var_entries: Vec<_> = inner_prepare.free_var_map.into_iter().collect();\n        free_var_entries.sort_by_key(|(_, our_slot)| *our_slot);\n\n        let free_var_enclosing_slots: Vec<NamespaceId> = free_var_entries\n            .into_iter()\n            .map(|(var_name, _our_slot)| {\n                if let Some(&slot) = self.cell_var_map.get(&var_name) {\n                    slot\n                } else if let Some(&slot) = self.free_var_map.get(&var_name) {\n                    slot\n                } else {\n                    panic!(\"free_var '{var_name}' not found in enclosing scope's cell_var_map or free_var_map\");\n                }\n            })\n            .collect();\n\n        // Build cell_param_indices\n        let cell_var_count = inner_prepare.cell_var_map.len();\n        let namespace_size = inner_prepare.namespace_size;\n\n        let cell_param_indices: Vec<Option<usize>> = if cell_var_count == 0 {\n            Vec::new()\n        } else {\n            let param_name_to_index: AHashMap<String, usize> = param_names\n                .iter()\n                .enumerate()\n                .map(|(idx, &name_id)| (self.interner.get_str(name_id).to_string(), idx))\n                .collect();\n\n            let mut cell_entries: Vec<_> = inner_prepare.cell_var_map.iter().collect();\n            cell_entries.sort_by_key(|&(_, slot)| slot);\n\n            cell_entries\n                .into_iter()\n                .map(|(name, _slot)| param_name_to_index.get(name).copied())\n                .collect()\n        };\n\n        // Build the runtime Signature from the parsed signature\n        let pos_args: Vec<StringId> = parsed_sig.pos_args.iter().map(|p| p.name).collect();\n        let pos_defaults_count = parsed_sig.pos_args.iter().filter(|p| p.default.is_some()).count();\n        let args: Vec<StringId> = parsed_sig.args.iter().map(|p| p.name).collect();\n        let arg_defaults_count = parsed_sig.args.iter().filter(|p| p.default.is_some()).count();\n        let mut kwargs: Vec<StringId> = Vec::with_capacity(parsed_sig.kwargs.len());\n        let mut kwarg_default_map: Vec<Option<usize>> = Vec::with_capacity(parsed_sig.kwargs.len());\n        let mut kwarg_default_index = 0;\n        for param in &parsed_sig.kwargs {\n            kwargs.push(param.name);\n            if param.default.is_some() {\n                kwarg_default_map.push(Some(kwarg_default_index));\n                kwarg_default_index += 1;\n            } else {\n                kwarg_default_map.push(None);\n            }\n        }\n\n        let signature = Signature::new(\n            pos_args,\n            pos_defaults_count,\n            args,\n            arg_defaults_count,\n            parsed_sig.var_args,\n            kwargs,\n            kwarg_default_map,\n            parsed_sig.var_kwargs,\n        );\n\n        // Collect and prepare default expressions (evaluated in enclosing scope)\n        let mut default_exprs = Vec::with_capacity(signature.total_defaults_count());\n        for param in &parsed_sig.pos_args {\n            if let Some(ref expr) = param.default {\n                default_exprs.push(self.prepare_expression(expr.clone())?);\n            }\n        }\n        for param in &parsed_sig.args {\n            if let Some(ref expr) = param.default {\n                default_exprs.push(self.prepare_expression(expr.clone())?);\n            }\n        }\n        for param in &parsed_sig.kwargs {\n            if let Some(ref expr) = param.default {\n                default_exprs.push(self.prepare_expression(expr.clone())?);\n            }\n        }\n\n        // Create the prepared function definition (lambdas are never async)\n        let func_def = PreparedFunctionDef {\n            name: lambda_name,\n            signature,\n            body: prepared_body,\n            namespace_size,\n            free_var_enclosing_slots,\n            cell_var_count,\n            cell_param_indices,\n            default_exprs,\n            is_async: false,\n        };\n\n        Ok(ExprLoc::new(\n            position,\n            Expr::Lambda {\n                func_def: Box::new(func_def),\n            },\n        ))\n    }\n\n    /// Resolves an identifier to its namespace index and scope, creating a new entry if needed.\n    ///\n    /// TODO This whole implementation seems ugly at best.\n    ///\n    /// This is the core name resolution mechanism with scope-aware resolution:\n    ///\n    /// **At module level:** All names go to the local namespace (which IS the global namespace).\n    ///\n    /// **In functions:**\n    /// - If name is declared `global` → resolve to global namespace\n    /// - If name is declared `nonlocal` → resolve to enclosing scope via Cell\n    /// - If name is assigned in this function → resolve to local namespace\n    /// - If name exists in global namespace (read-only access) → resolve to global namespace\n    /// - Otherwise → resolve to local namespace (will be NameError at runtime)\n    ///\n    /// # Returns\n    /// A tuple of (resolved Identifier with id and scope set, whether this is a new local name).\n    fn get_id(&mut self, ident: Identifier) -> (Identifier, bool) {\n        let name_str = self.interner.get_str(ident.name_id);\n\n        // At module level, all names are local (which is also the global namespace).\n        // The compiler emits global opcodes for these, so the VM reads/writes\n        // directly from the globals array rather than the stack.\n        if self.is_module_scope {\n            return match self.name_map.entry(name_str.to_string()) {\n                Entry::Occupied(e) => {\n                    // Name already exists (from prior reference or pre-registered).\n                    // Determine scope the same way as for vacant entries: if the name\n                    // has been assigned so far, it's a true local; otherwise it's an\n                    // unassigned reference that should yield NameLookup at runtime.\n                    let scope = if self.names_assigned_in_order.contains(name_str) {\n                        NameScope::Local\n                    } else {\n                        NameScope::LocalUnassigned\n                    };\n                    (\n                        Identifier::new_with_scope(ident.name_id, ident.position, *e.get(), scope),\n                        false,\n                    )\n                }\n                Entry::Vacant(e) => {\n                    let id = NamespaceId::new(self.namespace_size);\n                    self.namespace_size += 1;\n                    e.insert(id);\n                    // Determine scope: if the name is assigned somewhere (even later in the file),\n                    // it's a true local that will raise UnboundLocalError if accessed before assignment.\n                    // If the name is never assigned, it's an undefined reference that raises NameError.\n                    let scope = if self.names_assigned_in_order.contains(name_str) {\n                        NameScope::Local\n                    } else {\n                        NameScope::LocalUnassigned\n                    };\n                    (\n                        Identifier::new_with_scope(ident.name_id, ident.position, id, scope),\n                        true,\n                    )\n                }\n            };\n        }\n\n        // In a function: determine scope based on global_names, nonlocal_names, assigned_names, global_name_map\n\n        // 1. Check if declared `global`\n        if self.global_names.contains(name_str) {\n            if let Some(ref global_map) = self.global_name_map\n                && let Some(&global_id) = global_map.get(name_str)\n            {\n                // Name exists in global namespace\n                return (\n                    Identifier::new_with_scope(ident.name_id, ident.position, global_id, NameScope::Global),\n                    false,\n                );\n            }\n            // Declared global but doesn't exist yet - it will be created when assigned\n            // For now, we still need a global index. We'll use a placeholder approach:\n            // allocate in global namespace (this is a simplification - in real Python,\n            // the global would be created at module level when first assigned)\n            // For our implementation, we'll resolve to global but the variable won't exist until assigned.\n            // Return a \"new\" global - but we can't modify global_name_map here.\n            // For simplicity, we'll resolve to local with Global scope - runtime will handle the lookup.\n            let (id, is_new) = match self.name_map.entry(name_str.to_string()) {\n                Entry::Occupied(e) => (*e.get(), false),\n                Entry::Vacant(e) => {\n                    let id = NamespaceId::new(self.namespace_size);\n                    self.namespace_size += 1;\n                    e.insert(id);\n                    (id, true)\n                }\n            };\n            // Mark as Global scope - runtime will need to handle this specially\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, id, NameScope::Global),\n                is_new,\n            );\n        }\n\n        // 2. Check if captured from enclosing scope (nonlocal declaration or implicit capture)\n        // free_var_map stores namespace slot indices where the cell reference will be stored\n        if let Some(&slot) = self.free_var_map.get(name_str) {\n            // At runtime, the cell reference is in namespace[slot] as Value::Ref(cell_id)\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, slot, NameScope::Cell),\n                false, // Not a new local - it's captured from enclosing scope\n            );\n        }\n\n        // 3. Check if this is a cell variable (captured by nested functions)\n        // cell_var_map stores namespace slot indices where the cell reference will be stored\n        // At call time, a cell is created and stored as Value::Ref(cell_id) at this slot\n        if let Some(&slot) = self.cell_var_map.get(name_str) {\n            // The namespace slot was already allocated when cell_var_map was populated\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, slot, NameScope::Cell),\n                false, // Not a \"new\" local - it's a cell variable\n            );\n        }\n\n        // 4. Check if assigned in this function (local variable)\n        if self.assigned_names.contains(name_str) {\n            let (id, is_new) = match self.name_map.entry(name_str.to_string()) {\n                Entry::Occupied(e) => (*e.get(), false),\n                Entry::Vacant(e) => {\n                    let id = NamespaceId::new(self.namespace_size);\n                    self.namespace_size += 1;\n                    e.insert(id);\n                    (id, true)\n                }\n            };\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, id, NameScope::Local),\n                is_new,\n            );\n        }\n\n        // 5. Check if name was pre-populated in name_map (from function parameters)\n        // This ensures parameters shadow both enclosing locals and global variables\n        // with the same name. Parameters are added to name_map during\n        // FunctionScope::new_function() but are NOT in assigned_names (since they're\n        // not assigned in the function body). This MUST be checked before\n        // enclosing_locals, otherwise a parameter like `def inner(x)` would be\n        // incorrectly resolved as a closure capture when an outer scope also has `x`.\n        // Excludes names tracked in `unassigned_ref_names` — those were added to\n        // `name_map` by step 8 as `LocalUnassigned` references and must stay that way\n        // to trigger NameLookup at runtime (e.g., for external function resolution).\n        if !self.unassigned_ref_names.contains(name_str)\n            && let Some(&id) = self.name_map.get(name_str)\n        {\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, id, NameScope::Local),\n                false, // Not new - was pre-populated from parameters\n            );\n        }\n\n        // 6. Check if exists in enclosing scope (implicit closure capture)\n        // This handles reading variables from enclosing functions without explicit `nonlocal`\n        if let Some(ref enclosing) = self.enclosing_locals\n            && enclosing.contains(name_str)\n        {\n            // This is an implicit capture - add to free_var_map with a namespace slot\n            let slot = if let Some(&existing_slot) = self.free_var_map.get(name_str) {\n                existing_slot\n            } else {\n                // Allocate a namespace slot for this free variable\n                let slot = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n                self.name_map.insert(name_str.to_string(), slot);\n                self.free_var_map.insert(name_str.to_string(), slot);\n                slot\n            };\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, slot, NameScope::Cell),\n                false, // Not a new local - it's captured from enclosing scope\n            );\n        }\n\n        // 7. Check if exists in global namespace (implicit global read)\n        if let Some(ref global_map) = self.global_name_map\n            && let Some(&global_id) = global_map.get(name_str)\n        {\n            return (\n                Identifier::new_with_scope(ident.name_id, ident.position, global_id, NameScope::Global),\n                false,\n            );\n        }\n\n        // 8. Name not found anywhere - allocate a local slot (will be NameError at runtime)\n        // This handles names that are only read (never assigned) and don't exist globally.\n        // We allocate a local slot that will never be written to.\n        // Mark as LocalUnassigned so runtime raises NameError (not UnboundLocalError).\n        // Track in `unassigned_ref_names` so step 6 doesn't treat subsequent references\n        // as `Local` (parameters).\n        self.unassigned_ref_names.insert(name_str.to_string());\n        let (id, is_new) = match self.name_map.entry(name_str.to_string()) {\n            Entry::Occupied(e) => (*e.get(), false),\n            Entry::Vacant(e) => {\n                let id = NamespaceId::new(self.namespace_size);\n                self.namespace_size += 1;\n                e.insert(id);\n                (id, true)\n            }\n        };\n        (\n            Identifier::new_with_scope(ident.name_id, ident.position, id, NameScope::LocalUnassigned),\n            is_new,\n        )\n    }\n\n    /// Prepares an f-string part by resolving names in interpolated expressions.\n    fn prepare_fstring_part(&mut self, part: FStringPart) -> Result<FStringPart, ParseError> {\n        match part {\n            FStringPart::Literal(s) => Ok(FStringPart::Literal(s)),\n            FStringPart::Interpolation {\n                expr,\n                conversion,\n                format_spec,\n                debug_prefix,\n            } => {\n                let prepared_expr = Box::new(self.prepare_expression(*expr)?);\n                let prepared_spec = match format_spec {\n                    Some(FormatSpec::Static(s)) => Some(FormatSpec::Static(s)),\n                    Some(FormatSpec::Dynamic(parts)) => {\n                        let prepared = parts\n                            .into_iter()\n                            .map(|p| self.prepare_fstring_part(p))\n                            .collect::<Result<Vec<_>, _>>()?;\n                        Some(FormatSpec::Dynamic(prepared))\n                    }\n                    None => None,\n                };\n                Ok(FStringPart::Interpolation {\n                    expr: prepared_expr,\n                    conversion,\n                    format_spec: prepared_spec,\n                    debug_prefix,\n                })\n            }\n        }\n    }\n}\n\n/// Information collected from first-pass scan of a function body.\n///\n/// This struct holds the scope-related information needed for the second pass\n/// of function preparation and for closure analysis.\nstruct FunctionScopeInfo {\n    /// Names declared as `global`\n    global_names: AHashSet<String>,\n    /// Names declared as `nonlocal`\n    nonlocal_names: AHashSet<String>,\n    /// Names that are assigned in this scope\n    assigned_names: AHashSet<String>,\n    /// Names that are captured by nested functions (must be stored in cells)\n    cell_var_names: AHashSet<String>,\n    /// Names that are referenced but not local, global, or nonlocal.\n    /// These are POTENTIAL implicit captures - they may be captures from an enclosing function\n    /// OR they may be builtin/global reads. The actual implicit captures are determined\n    /// by filtering against enclosing_locals in new_function.\n    potential_captures: AHashSet<String>,\n}\n\n/// Scans a function body to collect scope information (first phase of preparation).\n///\n/// This function performs three passes over the AST:\n/// 1. Collect global, nonlocal, and assigned names\n/// 2. Identify cell_vars (names captured by nested functions)\n/// 3. Collect potential implicit captures (referenced but not local/global/nonlocal)\n///\n/// The collected information includes:\n/// - Names declared as `global` (from Global statements)\n/// - Names declared as `nonlocal` (from Nonlocal statements)\n/// - Names that are assigned (from Assign, OpAssign, For targets, etc.)\n/// - Names that are captured by nested functions (cell_var_names)\n/// - Names that might be captured from enclosing scope (potential_captures)\n///\n/// This information is used to determine whether each name reference should resolve\n/// to the local namespace, global namespace, or an enclosing scope via cells.\nfn collect_function_scope_info(\n    nodes: &[ParseNode],\n    params: &[StringId],\n    interner: &InternerBuilder,\n) -> FunctionScopeInfo {\n    let mut global_names = AHashSet::new();\n    let mut nonlocal_names = AHashSet::new();\n    let mut assigned_names = AHashSet::new();\n    let mut cell_var_names = AHashSet::new();\n    let mut referenced_names = AHashSet::new();\n\n    // First pass: collect global, nonlocal, and assigned names\n    for node in nodes {\n        collect_scope_info_from_node(\n            node,\n            &mut global_names,\n            &mut nonlocal_names,\n            &mut assigned_names,\n            interner,\n        );\n    }\n\n    // Build the set of our locals: params + assigned_names (excluding globals)\n    let param_names: AHashSet<String> = params\n        .iter()\n        .map(|string_id| interner.get_str(*string_id).to_string())\n        .collect();\n\n    let our_locals: AHashSet<String> = param_names\n        .iter()\n        .cloned()\n        .chain(assigned_names.iter().cloned())\n        .filter(|name| !global_names.contains(name))\n        .collect();\n\n    // Second pass: find what nested functions capture from us\n    for node in nodes {\n        collect_cell_vars_from_node(node, &our_locals, &mut cell_var_names, interner);\n    }\n\n    // Third pass: collect all referenced names to identify potential implicit captures.\n    // These are names that might be captured from an enclosing function scope.\n    // We can't fully determine implicit captures here because we don't know yet what\n    // the enclosing scope's locals are - that's determined later when we call new_function.\n    for node in nodes {\n        collect_referenced_names_from_node(node, &mut referenced_names, interner);\n    }\n\n    // Potential implicit captures are names that are:\n    // - Referenced in the function body\n    // - Not local (not params, not assigned)\n    // - Not declared global\n    // - Not declared nonlocal (those are handled separately)\n    // The actual implicit captures will be filtered against enclosing_locals in new_function.\n    let potential_captures: AHashSet<String> = referenced_names\n        .into_iter()\n        .filter(|name| !our_locals.contains(name) && !global_names.contains(name) && !nonlocal_names.contains(name))\n        .collect();\n\n    FunctionScopeInfo {\n        global_names,\n        nonlocal_names,\n        assigned_names,\n        cell_var_names,\n        potential_captures,\n    }\n}\n\n/// Helper to collect scope info from a single node.\nfn collect_scope_info_from_node(\n    node: &ParseNode,\n    global_names: &mut AHashSet<String>,\n    nonlocal_names: &mut AHashSet<String>,\n    assigned_names: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    match node {\n        Node::Global { names, .. } => {\n            for string_id in names {\n                global_names.insert(interner.get_str(*string_id).to_string());\n            }\n        }\n        Node::Nonlocal { names, .. } => {\n            for string_id in names {\n                nonlocal_names.insert(interner.get_str(*string_id).to_string());\n            }\n        }\n        Node::Assign { target, object } => {\n            assigned_names.insert(interner.get_str(target.name_id).to_string());\n            // Scan value expression for walrus operators\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n        }\n        Node::UnpackAssign { targets, object, .. } => {\n            // Recursively collect all names from nested unpack targets\n            for target in targets {\n                collect_names_from_unpack_target(target, assigned_names, interner);\n            }\n            // Scan value expression for walrus operators\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n        }\n        Node::OpAssign { target, object, .. } => {\n            assigned_names.insert(interner.get_str(target.name_id).to_string());\n            // Scan value expression for walrus operators\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n        }\n        Node::SubscriptOpAssign { index, object, .. } => {\n            collect_assigned_names_from_expr(index, assigned_names, interner);\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n        }\n        Node::SubscriptAssign { index, value, .. } => {\n            // Subscript assignment doesn't create a new name, it modifies existing container\n            // But scan expressions for walrus operators\n            collect_assigned_names_from_expr(index, assigned_names, interner);\n            collect_assigned_names_from_expr(value, assigned_names, interner);\n        }\n        Node::AttrAssign { object, value, .. } => {\n            // Attribute assignment doesn't create a new name, it modifies existing object\n            // But scan expressions for walrus operators\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n            collect_assigned_names_from_expr(value, assigned_names, interner);\n        }\n        Node::For {\n            target,\n            iter,\n            body,\n            or_else,\n        } => {\n            // For loop target is assigned - collect all names from the target\n            collect_names_from_unpack_target(target, assigned_names, interner);\n            // Scan iter expression for walrus operators\n            collect_assigned_names_from_expr(iter, assigned_names, interner);\n            // Recurse into body and else\n            for n in body {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n            for n in or_else {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n        }\n        Node::While { test, body, or_else } => {\n            // Scan test expression for walrus operators\n            collect_assigned_names_from_expr(test, assigned_names, interner);\n            // Recurse into body and else blocks\n            for n in body {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n            for n in or_else {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n        }\n        Node::If { test, body, or_else } => {\n            // Scan test expression for walrus operators\n            collect_assigned_names_from_expr(test, assigned_names, interner);\n            // Recurse into branches\n            for n in body {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n            for n in or_else {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n        }\n        Node::FunctionDef(RawFunctionDef { name, .. }) => {\n            // Function definition creates a local binding for the function name\n            // But we don't recurse into the function body - that's a separate scope\n            assigned_names.insert(interner.get_str(name.name_id).to_string());\n        }\n        Node::Try(Try {\n            body,\n            handlers,\n            or_else,\n            finally,\n        }) => {\n            // Recurse into all blocks\n            for n in body {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n            for handler in handlers {\n                // Exception variable name is assigned\n                if let Some(ref name) = handler.name {\n                    assigned_names.insert(interner.get_str(name.name_id).to_string());\n                }\n                for n in &handler.body {\n                    collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n                }\n            }\n            for n in or_else {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n            for n in finally {\n                collect_scope_info_from_node(n, global_names, nonlocal_names, assigned_names, interner);\n            }\n        }\n        // Import creates a binding for the module name (or alias)\n        Node::Import { binding, .. } => {\n            assigned_names.insert(interner.get_str(binding.name_id).to_string());\n        }\n        // ImportFrom creates bindings for each imported name (or alias)\n        Node::ImportFrom { names, .. } => {\n            for (_import_name, binding) in names {\n                assigned_names.insert(interner.get_str(binding.name_id).to_string());\n            }\n        }\n        // Statements with expressions that may contain walrus operators\n        Node::Expr(expr) | Node::Return(expr) => {\n            collect_assigned_names_from_expr(expr, assigned_names, interner);\n        }\n        Node::Raise(Some(expr)) => {\n            collect_assigned_names_from_expr(expr, assigned_names, interner);\n        }\n        Node::Assert { test, msg } => {\n            collect_assigned_names_from_expr(test, assigned_names, interner);\n            if let Some(m) = msg {\n                collect_assigned_names_from_expr(m, assigned_names, interner);\n            }\n        }\n        // These don't create new names\n        Node::Pass | Node::ReturnNone | Node::Raise(None) | Node::Break { .. } | Node::Continue { .. } => {}\n    }\n}\n\n/// Collects names assigned by walrus operators (`:=`) within an expression.\n///\n/// Per PEP 572, walrus operator targets are assignments in the enclosing scope.\n/// This function recursively scans expressions to find all `Named` expression targets.\n/// It does NOT recurse into lambda bodies as those have their own scope.\nfn collect_assigned_names_from_expr(expr: &ExprLoc, assigned_names: &mut AHashSet<String>, interner: &InternerBuilder) {\n    match &expr.expr {\n        Expr::Named { target, value } => {\n            // The target of a walrus operator is assigned in this scope\n            assigned_names.insert(interner.get_str(target.name_id).to_string());\n            // Also scan the value expression\n            collect_assigned_names_from_expr(value, assigned_names, interner);\n        }\n        // Recurse into sub-expressions\n        Expr::List(items) | Expr::Tuple(items) | Expr::Set(items) => {\n            for item in items {\n                let expr = match item {\n                    SequenceItem::Value(e) | SequenceItem::Unpack(e) => e,\n                };\n                collect_assigned_names_from_expr(expr, assigned_names, interner);\n            }\n        }\n        Expr::Dict(dict_items) => {\n            for item in dict_items {\n                match item {\n                    DictItem::Pair(key, value) => {\n                        collect_assigned_names_from_expr(key, assigned_names, interner);\n                        collect_assigned_names_from_expr(value, assigned_names, interner);\n                    }\n                    DictItem::Unpack(e) => collect_assigned_names_from_expr(e, assigned_names, interner),\n                }\n            }\n        }\n        Expr::Op { left, right, .. } | Expr::CmpOp { left, right, .. } => {\n            collect_assigned_names_from_expr(left, assigned_names, interner);\n            collect_assigned_names_from_expr(right, assigned_names, interner);\n        }\n        Expr::ChainCmp { left, comparisons } => {\n            collect_assigned_names_from_expr(left, assigned_names, interner);\n            for (_, expr) in comparisons {\n                collect_assigned_names_from_expr(expr, assigned_names, interner);\n            }\n        }\n        Expr::Not(operand)\n        | Expr::UnaryMinus(operand)\n        | Expr::UnaryPlus(operand)\n        | Expr::UnaryInvert(operand)\n        | Expr::Await(operand) => {\n            collect_assigned_names_from_expr(operand, assigned_names, interner);\n        }\n        Expr::Subscript { object, index } => {\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n            collect_assigned_names_from_expr(index, assigned_names, interner);\n        }\n        Expr::Call { args, .. } => {\n            collect_assigned_names_from_args(args, assigned_names, interner);\n        }\n        Expr::AttrCall { object, args, .. } => {\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n            collect_assigned_names_from_args(args, assigned_names, interner);\n        }\n        Expr::IndirectCall { callable, args } => {\n            collect_assigned_names_from_expr(callable, assigned_names, interner);\n            collect_assigned_names_from_args(args, assigned_names, interner);\n        }\n        Expr::AttrGet { object, .. } => {\n            collect_assigned_names_from_expr(object, assigned_names, interner);\n        }\n        Expr::IfElse { test, body, orelse } => {\n            collect_assigned_names_from_expr(test, assigned_names, interner);\n            collect_assigned_names_from_expr(body, assigned_names, interner);\n            collect_assigned_names_from_expr(orelse, assigned_names, interner);\n        }\n        // Per PEP 572, walrus in comprehensions assigns to the ENCLOSING scope\n        Expr::ListComp { elt, generators } | Expr::SetComp { elt, generators } => {\n            collect_assigned_names_from_expr(elt, assigned_names, interner);\n            for generator in generators {\n                collect_assigned_names_from_expr(&generator.iter, assigned_names, interner);\n                for cond in &generator.ifs {\n                    collect_assigned_names_from_expr(cond, assigned_names, interner);\n                }\n            }\n        }\n        Expr::DictComp { key, value, generators } => {\n            collect_assigned_names_from_expr(key, assigned_names, interner);\n            collect_assigned_names_from_expr(value, assigned_names, interner);\n            for generator in generators {\n                collect_assigned_names_from_expr(&generator.iter, assigned_names, interner);\n                for cond in &generator.ifs {\n                    collect_assigned_names_from_expr(cond, assigned_names, interner);\n                }\n            }\n        }\n        Expr::FString(parts) => {\n            for part in parts {\n                if let FStringPart::Interpolation { expr, .. } = part {\n                    collect_assigned_names_from_expr(expr, assigned_names, interner);\n                }\n            }\n        }\n        Expr::Slice { lower, upper, step } => {\n            if let Some(e) = lower {\n                collect_assigned_names_from_expr(e, assigned_names, interner);\n            }\n            if let Some(e) = upper {\n                collect_assigned_names_from_expr(e, assigned_names, interner);\n            }\n            if let Some(e) = step {\n                collect_assigned_names_from_expr(e, assigned_names, interner);\n            }\n        }\n        // Lambda bodies have their own scope - walrus inside them doesn't affect us\n        Expr::LambdaRaw { .. } | Expr::Lambda { .. } => {}\n        // Leaf expressions don't contain walrus operators\n        Expr::Literal(_) | Expr::Builtin(_) | Expr::Name(_) => {}\n    }\n}\n\n/// Helper to collect assigned names from argument expressions.\nfn collect_assigned_names_from_args(\n    args: &ArgExprs,\n    assigned_names: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    match args {\n        ArgExprs::Empty => {}\n        ArgExprs::One(arg) => collect_assigned_names_from_expr(arg, assigned_names, interner),\n        ArgExprs::Two(arg1, arg2) => {\n            collect_assigned_names_from_expr(arg1, assigned_names, interner);\n            collect_assigned_names_from_expr(arg2, assigned_names, interner);\n        }\n        ArgExprs::Args(args) => {\n            for arg in args {\n                collect_assigned_names_from_expr(arg, assigned_names, interner);\n            }\n        }\n        ArgExprs::Kwargs(kwargs) => {\n            for kwarg in kwargs {\n                collect_assigned_names_from_expr(&kwarg.value, assigned_names, interner);\n            }\n        }\n        ArgExprs::ArgsKargs {\n            args,\n            kwargs,\n            var_args,\n            var_kwargs,\n        } => {\n            if let Some(args) = args {\n                for arg in args {\n                    collect_assigned_names_from_expr(arg, assigned_names, interner);\n                }\n            }\n            if let Some(kwargs) = kwargs {\n                for kwarg in kwargs {\n                    collect_assigned_names_from_expr(&kwarg.value, assigned_names, interner);\n                }\n            }\n            if let Some(var_args) = var_args {\n                collect_assigned_names_from_expr(var_args, assigned_names, interner);\n            }\n            if let Some(var_kwargs) = var_kwargs {\n                collect_assigned_names_from_expr(var_kwargs, assigned_names, interner);\n            }\n        }\n        ArgExprs::GeneralizedCall { args, kwargs } => {\n            for arg in args {\n                match arg {\n                    CallArg::Value(e) | CallArg::Unpack(e) => {\n                        collect_assigned_names_from_expr(e, assigned_names, interner);\n                    }\n                }\n            }\n            for kwarg in kwargs {\n                match kwarg {\n                    CallKwarg::Named(kw) => {\n                        collect_assigned_names_from_expr(&kw.value, assigned_names, interner);\n                    }\n                    CallKwarg::Unpack(e) => {\n                        collect_assigned_names_from_expr(e, assigned_names, interner);\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Collects cell_vars by analyzing what nested functions capture from our scope.\n///\n/// For each FunctionDef node, we recursively analyze its body to find what names it\n/// references. Any name that is in `our_locals` and referenced by the nested function\n/// (not as a local of the nested function) becomes a cell_var.\nfn collect_cell_vars_from_node(\n    node: &ParseNode,\n    our_locals: &AHashSet<String>,\n    cell_vars: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    match node {\n        Node::FunctionDef(RawFunctionDef { signature, body, .. }) => {\n            // Find what names are referenced inside this nested function\n            let mut referenced = AHashSet::new();\n            for n in body {\n                collect_referenced_names_from_node(n, &mut referenced, interner);\n            }\n\n            // Extract param names from signature for scope analysis\n            let param_names: Vec<StringId> = signature.param_names().collect();\n\n            // Collect the nested function's own locals (params + assigned)\n            let nested_scope = collect_function_scope_info(body, &param_names, interner);\n\n            // Any name that is:\n            // - Referenced by the nested function\n            // - Not a local of the nested function\n            // - Not declared global in the nested function\n            // - In our locals\n            // becomes a cell_var\n            for name in &referenced {\n                if !nested_scope.assigned_names.contains(name)\n                    && !param_names.iter().any(|p| interner.get_str(*p) == name)\n                    && !nested_scope.global_names.contains(name)\n                    && our_locals.contains(name)\n                {\n                    cell_vars.insert(name.clone());\n                }\n            }\n\n            // Also check what the nested function explicitly declares as nonlocal\n            for name in &nested_scope.nonlocal_names {\n                if our_locals.contains(name) {\n                    cell_vars.insert(name.clone());\n                }\n            }\n        }\n        // Recurse into control flow structures\n        Node::For { body, or_else, .. } => {\n            for n in body {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n            for n in or_else {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n        }\n        Node::While { body, or_else, .. } => {\n            for n in body {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n            for n in or_else {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n        }\n        Node::If { body, or_else, .. } => {\n            for n in body {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n            for n in or_else {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n        }\n        Node::Try(Try {\n            body,\n            handlers,\n            or_else,\n            finally,\n        }) => {\n            for n in body {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n            for handler in handlers {\n                for n in &handler.body {\n                    collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n                }\n            }\n            for n in or_else {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n            for n in finally {\n                collect_cell_vars_from_node(n, our_locals, cell_vars, interner);\n            }\n        }\n        // Handle expressions that may contain lambdas\n        Node::Expr(expr) | Node::Return(expr) => {\n            collect_cell_vars_from_expr(expr, our_locals, cell_vars, interner);\n        }\n        Node::Assign { object, .. } | Node::UnpackAssign { object, .. } => {\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n        }\n        Node::OpAssign { object, .. } => {\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n        }\n        Node::SubscriptOpAssign { index, object, .. } => {\n            collect_cell_vars_from_expr(index, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n        }\n        Node::SubscriptAssign { index, value, .. } => {\n            collect_cell_vars_from_expr(index, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(value, our_locals, cell_vars, interner);\n        }\n        Node::AttrAssign { object, value, .. } => {\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(value, our_locals, cell_vars, interner);\n        }\n        // Other nodes don't contain nested function definitions or lambdas\n        _ => {}\n    }\n}\n\n/// Collects cell_vars from lambda expressions within an expression.\n///\n/// Recursively searches through an expression tree to find lambda expressions\n/// that capture variables from the enclosing scope.\nfn collect_cell_vars_from_expr(\n    expr: &ExprLoc,\n    our_locals: &AHashSet<String>,\n    cell_vars: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    use crate::expressions::Expr;\n    match &expr.expr {\n        Expr::LambdaRaw { signature, body, .. } => {\n            // This lambda captures variables from our scope\n            // Find what names are referenced in the lambda body\n            let mut referenced = AHashSet::new();\n            collect_referenced_names_from_expr(body, &mut referenced, interner);\n            // Also collect from default expressions\n            for param in &signature.pos_args {\n                if let Some(ref default) = param.default {\n                    collect_referenced_names_from_expr(default, &mut referenced, interner);\n                }\n            }\n            for param in &signature.args {\n                if let Some(ref default) = param.default {\n                    collect_referenced_names_from_expr(default, &mut referenced, interner);\n                }\n            }\n            for param in &signature.kwargs {\n                if let Some(ref default) = param.default {\n                    collect_referenced_names_from_expr(default, &mut referenced, interner);\n                }\n            }\n\n            // Extract param names from signature\n            let param_names: Vec<StringId> = signature.param_names().collect();\n\n            // Any name that is:\n            // - Referenced by the lambda\n            // - Not a param of the lambda\n            // - In our locals\n            // becomes a cell_var\n            for name in &referenced {\n                if !param_names.iter().any(|p| interner.get_str(*p) == name) && our_locals.contains(name) {\n                    cell_vars.insert(name.clone());\n                }\n            }\n\n            // Recursively check the lambda body for nested lambdas.\n            // For nested lambdas, extend our_locals to include this lambda's parameters\n            // so that inner lambdas can find them for closure capture.\n            let mut extended_locals = our_locals.clone();\n            for param_id in &param_names {\n                extended_locals.insert(interner.get_str(*param_id).to_string());\n            }\n            collect_cell_vars_from_expr(body, &extended_locals, cell_vars, interner);\n        }\n        // Recurse into sub-expressions\n        Expr::List(items) | Expr::Tuple(items) | Expr::Set(items) => {\n            for item in items {\n                let expr = match item {\n                    SequenceItem::Value(e) | SequenceItem::Unpack(e) => e,\n                };\n                collect_cell_vars_from_expr(expr, our_locals, cell_vars, interner);\n            }\n        }\n        Expr::Dict(dict_items) => {\n            for item in dict_items {\n                match item {\n                    DictItem::Pair(key, value) => {\n                        collect_cell_vars_from_expr(key, our_locals, cell_vars, interner);\n                        collect_cell_vars_from_expr(value, our_locals, cell_vars, interner);\n                    }\n                    DictItem::Unpack(e) => collect_cell_vars_from_expr(e, our_locals, cell_vars, interner),\n                }\n            }\n        }\n        Expr::Op { left, right, .. } | Expr::CmpOp { left, right, .. } => {\n            collect_cell_vars_from_expr(left, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(right, our_locals, cell_vars, interner);\n        }\n        Expr::ChainCmp { left, comparisons } => {\n            collect_cell_vars_from_expr(left, our_locals, cell_vars, interner);\n            for (_, expr) in comparisons {\n                collect_cell_vars_from_expr(expr, our_locals, cell_vars, interner);\n            }\n        }\n        Expr::Not(operand) | Expr::UnaryMinus(operand) | Expr::UnaryPlus(operand) | Expr::UnaryInvert(operand) => {\n            collect_cell_vars_from_expr(operand, our_locals, cell_vars, interner);\n        }\n        Expr::Subscript { object, index } => {\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(index, our_locals, cell_vars, interner);\n        }\n        Expr::Call { args, .. } => {\n            collect_cell_vars_from_args(args, our_locals, cell_vars, interner);\n        }\n        Expr::AttrCall { object, args, .. } => {\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n            collect_cell_vars_from_args(args, our_locals, cell_vars, interner);\n        }\n        Expr::IndirectCall { callable, args } => {\n            collect_cell_vars_from_expr(callable, our_locals, cell_vars, interner);\n            collect_cell_vars_from_args(args, our_locals, cell_vars, interner);\n        }\n        Expr::AttrGet { object, .. } => {\n            collect_cell_vars_from_expr(object, our_locals, cell_vars, interner);\n        }\n        Expr::IfElse { test, body, orelse } => {\n            collect_cell_vars_from_expr(test, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(body, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(orelse, our_locals, cell_vars, interner);\n        }\n        Expr::ListComp { elt, generators } | Expr::SetComp { elt, generators } => {\n            collect_cell_vars_from_expr(elt, our_locals, cell_vars, interner);\n            for generator in generators {\n                collect_cell_vars_from_expr(&generator.iter, our_locals, cell_vars, interner);\n                for cond in &generator.ifs {\n                    collect_cell_vars_from_expr(cond, our_locals, cell_vars, interner);\n                }\n            }\n        }\n        Expr::DictComp { key, value, generators } => {\n            collect_cell_vars_from_expr(key, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(value, our_locals, cell_vars, interner);\n            for generator in generators {\n                collect_cell_vars_from_expr(&generator.iter, our_locals, cell_vars, interner);\n                for cond in &generator.ifs {\n                    collect_cell_vars_from_expr(cond, our_locals, cell_vars, interner);\n                }\n            }\n        }\n        Expr::FString(parts) => {\n            for part in parts {\n                if let crate::fstring::FStringPart::Interpolation { expr, .. } = part {\n                    collect_cell_vars_from_expr(expr, our_locals, cell_vars, interner);\n                }\n            }\n        }\n        Expr::Named { value, .. } => {\n            // Only scan the value expression for cell vars\n            collect_cell_vars_from_expr(value, our_locals, cell_vars, interner);\n        }\n        Expr::Await(value) => {\n            collect_cell_vars_from_expr(value, our_locals, cell_vars, interner);\n        }\n        // Leaf expressions\n        Expr::Literal(_) | Expr::Builtin(_) | Expr::Name(_) | Expr::Lambda { .. } | Expr::Slice { .. } => {}\n    }\n}\n\n/// Helper to collect cell vars from argument expressions.\nfn collect_cell_vars_from_args(\n    args: &ArgExprs,\n    our_locals: &AHashSet<String>,\n    cell_vars: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    match args {\n        ArgExprs::Empty => {}\n        ArgExprs::One(arg) => collect_cell_vars_from_expr(arg, our_locals, cell_vars, interner),\n        ArgExprs::Two(arg1, arg2) => {\n            collect_cell_vars_from_expr(arg1, our_locals, cell_vars, interner);\n            collect_cell_vars_from_expr(arg2, our_locals, cell_vars, interner);\n        }\n        ArgExprs::Args(args) => {\n            for arg in args {\n                collect_cell_vars_from_expr(arg, our_locals, cell_vars, interner);\n            }\n        }\n        ArgExprs::Kwargs(kwargs) => {\n            for kwarg in kwargs {\n                collect_cell_vars_from_expr(&kwarg.value, our_locals, cell_vars, interner);\n            }\n        }\n        ArgExprs::ArgsKargs {\n            args,\n            kwargs,\n            var_args,\n            var_kwargs,\n        } => {\n            if let Some(args) = args {\n                for arg in args {\n                    collect_cell_vars_from_expr(arg, our_locals, cell_vars, interner);\n                }\n            }\n            if let Some(kwargs) = kwargs {\n                for kwarg in kwargs {\n                    collect_cell_vars_from_expr(&kwarg.value, our_locals, cell_vars, interner);\n                }\n            }\n            if let Some(var_args) = var_args {\n                collect_cell_vars_from_expr(var_args, our_locals, cell_vars, interner);\n            }\n            if let Some(var_kwargs) = var_kwargs {\n                collect_cell_vars_from_expr(var_kwargs, our_locals, cell_vars, interner);\n            }\n        }\n        ArgExprs::GeneralizedCall { args, kwargs } => {\n            for arg in args {\n                match arg {\n                    CallArg::Value(e) | CallArg::Unpack(e) => {\n                        collect_cell_vars_from_expr(e, our_locals, cell_vars, interner);\n                    }\n                }\n            }\n            for kwarg in kwargs {\n                match kwarg {\n                    CallKwarg::Named(kw) => {\n                        collect_cell_vars_from_expr(&kw.value, our_locals, cell_vars, interner);\n                    }\n                    CallKwarg::Unpack(e) => {\n                        collect_cell_vars_from_expr(e, our_locals, cell_vars, interner);\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Collects all names referenced (read) in a node and its descendants.\n///\n/// This is used to find what names a nested function references from enclosing scopes.\nfn collect_referenced_names_from_node(node: &ParseNode, referenced: &mut AHashSet<String>, interner: &InternerBuilder) {\n    match node {\n        Node::Expr(expr) => collect_referenced_names_from_expr(expr, referenced, interner),\n        Node::Return(expr) => collect_referenced_names_from_expr(expr, referenced, interner),\n        Node::Raise(Some(expr)) => collect_referenced_names_from_expr(expr, referenced, interner),\n        Node::Raise(None) => {}\n        Node::Assert { test, msg } => {\n            collect_referenced_names_from_expr(test, referenced, interner);\n            if let Some(m) = msg {\n                collect_referenced_names_from_expr(m, referenced, interner);\n            }\n        }\n        Node::Assign { object, .. } => {\n            collect_referenced_names_from_expr(object, referenced, interner);\n        }\n        Node::UnpackAssign { object, .. } => {\n            collect_referenced_names_from_expr(object, referenced, interner);\n        }\n        Node::OpAssign { target, object, .. } => {\n            // OpAssign reads the target before writing\n            referenced.insert(interner.get_str(target.name_id).to_string());\n            collect_referenced_names_from_expr(object, referenced, interner);\n        }\n        Node::SubscriptOpAssign {\n            target, index, object, ..\n        } => {\n            referenced.insert(interner.get_str(target.name_id).to_string());\n            collect_referenced_names_from_expr(index, referenced, interner);\n            collect_referenced_names_from_expr(object, referenced, interner);\n        }\n        Node::SubscriptAssign {\n            target, index, value, ..\n        } => {\n            referenced.insert(interner.get_str(target.name_id).to_string());\n            collect_referenced_names_from_expr(index, referenced, interner);\n            collect_referenced_names_from_expr(value, referenced, interner);\n        }\n        Node::AttrAssign { object, value, .. } => {\n            collect_referenced_names_from_expr(object, referenced, interner);\n            collect_referenced_names_from_expr(value, referenced, interner);\n        }\n        Node::For {\n            iter, body, or_else, ..\n        } => {\n            collect_referenced_names_from_expr(iter, referenced, interner);\n            for n in body {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n            for n in or_else {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n        }\n        Node::While { test, body, or_else } => {\n            collect_referenced_names_from_expr(test, referenced, interner);\n            for n in body {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n            for n in or_else {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n        }\n        Node::If { test, body, or_else } => {\n            collect_referenced_names_from_expr(test, referenced, interner);\n            for n in body {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n            for n in or_else {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n        }\n        Node::FunctionDef(_) => {\n            // Don't recurse into nested function bodies - they have their own scope\n        }\n        Node::Try(Try {\n            body,\n            handlers,\n            or_else,\n            finally,\n        }) => {\n            for n in body {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n            for handler in handlers {\n                // Exception type expression may reference names\n                if let Some(ref exc_type) = handler.exc_type {\n                    collect_referenced_names_from_expr(exc_type, referenced, interner);\n                }\n                for n in &handler.body {\n                    collect_referenced_names_from_node(n, referenced, interner);\n                }\n            }\n            for n in or_else {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n            for n in finally {\n                collect_referenced_names_from_node(n, referenced, interner);\n            }\n        }\n        // Imports create bindings but don't reference names\n        Node::Import { .. } | Node::ImportFrom { .. } => {}\n        Node::Pass\n        | Node::ReturnNone\n        | Node::Global { .. }\n        | Node::Nonlocal { .. }\n        | Node::Break { .. }\n        | Node::Continue { .. } => {}\n    }\n}\n\n/// Collects all names referenced in an expression.\nfn collect_referenced_names_from_expr(\n    expr: &crate::expressions::ExprLoc,\n    referenced: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    use crate::expressions::Expr;\n    match &expr.expr {\n        Expr::Name(ident) => {\n            referenced.insert(interner.get_str(ident.name_id).to_string());\n        }\n        Expr::Literal(_) => {}\n        Expr::Builtin(_) => {}\n        Expr::List(items) | Expr::Tuple(items) | Expr::Set(items) => {\n            for item in items {\n                let expr = match item {\n                    SequenceItem::Value(e) | SequenceItem::Unpack(e) => e,\n                };\n                collect_referenced_names_from_expr(expr, referenced, interner);\n            }\n        }\n        Expr::Dict(dict_items) => {\n            for item in dict_items {\n                match item {\n                    DictItem::Pair(key, value) => {\n                        collect_referenced_names_from_expr(key, referenced, interner);\n                        collect_referenced_names_from_expr(value, referenced, interner);\n                    }\n                    DictItem::Unpack(e) => collect_referenced_names_from_expr(e, referenced, interner),\n                }\n            }\n        }\n        Expr::Op { left, right, .. } | Expr::CmpOp { left, right, .. } => {\n            collect_referenced_names_from_expr(left, referenced, interner);\n            collect_referenced_names_from_expr(right, referenced, interner);\n        }\n        Expr::ChainCmp { left, comparisons } => {\n            collect_referenced_names_from_expr(left, referenced, interner);\n            for (_, expr) in comparisons {\n                collect_referenced_names_from_expr(expr, referenced, interner);\n            }\n        }\n        Expr::Not(operand) | Expr::UnaryMinus(operand) | Expr::UnaryPlus(operand) | Expr::UnaryInvert(operand) => {\n            collect_referenced_names_from_expr(operand, referenced, interner);\n        }\n        Expr::FString(parts) => {\n            collect_referenced_names_from_fstring_parts(parts, referenced, interner);\n        }\n        Expr::Subscript { object, index } => {\n            collect_referenced_names_from_expr(object, referenced, interner);\n            collect_referenced_names_from_expr(index, referenced, interner);\n        }\n        Expr::Call { callable, args } => {\n            // Check if the callable is a Name reference\n            if let Callable::Name(ident) = callable {\n                referenced.insert(interner.get_str(ident.name_id).to_string());\n            }\n            collect_referenced_names_from_args(args, referenced, interner);\n        }\n        Expr::AttrCall { object, args, .. } => {\n            collect_referenced_names_from_expr(object, referenced, interner);\n            collect_referenced_names_from_args(args, referenced, interner);\n        }\n        Expr::AttrGet { object, .. } => {\n            collect_referenced_names_from_expr(object, referenced, interner);\n        }\n        Expr::IndirectCall { callable, args } => {\n            // Collect references from the callable expression and arguments\n            collect_referenced_names_from_expr(callable, referenced, interner);\n            collect_referenced_names_from_args(args, referenced, interner);\n        }\n        Expr::IfElse { test, body, orelse } => {\n            collect_referenced_names_from_expr(test, referenced, interner);\n            collect_referenced_names_from_expr(body, referenced, interner);\n            collect_referenced_names_from_expr(orelse, referenced, interner);\n        }\n        Expr::ListComp { elt, generators } | Expr::SetComp { elt, generators } => {\n            collect_referenced_names_from_comprehension(generators, Some(elt), None, referenced, interner);\n        }\n        Expr::DictComp { key, value, generators } => {\n            collect_referenced_names_from_comprehension(generators, None, Some((key, value)), referenced, interner);\n        }\n        Expr::LambdaRaw { signature, body, .. } => {\n            // Build set of parameter names (these are local to the lambda, not free variables)\n            let lambda_params: AHashSet<String> = signature\n                .param_names()\n                .map(|s| interner.get_str(s).to_string())\n                .collect();\n\n            // Collect references from the body expression into a temporary set\n            let mut body_refs: AHashSet<String> = AHashSet::new();\n            collect_referenced_names_from_expr(body, &mut body_refs, interner);\n\n            // Filter out the lambda's own parameters before adding to referenced set.\n            // The lambda's parameters are bound by the lambda, not free from outer scope.\n            for name in body_refs {\n                if !lambda_params.contains(&name) {\n                    referenced.insert(name);\n                }\n            }\n\n            // Default value expressions are evaluated in the enclosing scope, not the lambda's\n            // scope, so they can reference outer scope without filtering.\n            for param in &signature.pos_args {\n                if let Some(ref default) = param.default {\n                    collect_referenced_names_from_expr(default, referenced, interner);\n                }\n            }\n            for param in &signature.args {\n                if let Some(ref default) = param.default {\n                    collect_referenced_names_from_expr(default, referenced, interner);\n                }\n            }\n            for param in &signature.kwargs {\n                if let Some(ref default) = param.default {\n                    collect_referenced_names_from_expr(default, referenced, interner);\n                }\n            }\n        }\n        Expr::Lambda { .. } => {\n            // Lambda should only exist after preparation; this function operates on raw expressions\n            unreachable!(\"Expr::Lambda should not exist during scope analysis\")\n        }\n        Expr::Named { value, .. } => {\n            // Only the value is referenced; target is being assigned, not read\n            collect_referenced_names_from_expr(value, referenced, interner);\n        }\n        Expr::Slice { lower, upper, step } => {\n            if let Some(expr) = lower {\n                collect_referenced_names_from_expr(expr, referenced, interner);\n            }\n            if let Some(expr) = upper {\n                collect_referenced_names_from_expr(expr, referenced, interner);\n            }\n            if let Some(expr) = step {\n                collect_referenced_names_from_expr(expr, referenced, interner);\n            }\n        }\n        Expr::Await(value) => {\n            collect_referenced_names_from_expr(value, referenced, interner);\n        }\n    }\n}\n\n/// Collects referenced names from comprehension expressions.\n///\n/// Handles the special scoping rules: loop variables are local to the comprehension,\n/// so we collect references from iterators and conditions but exclude loop variable names.\nfn collect_referenced_names_from_comprehension(\n    generators: &[Comprehension],\n    elt: Option<&ExprLoc>,\n    key_value: Option<(&ExprLoc, &ExprLoc)>,\n    referenced: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    // Track loop variable names (these are local to the comprehension)\n    let mut comp_locals: AHashSet<String> = AHashSet::new();\n\n    // Collect references from expressions that can see prior loop variables.\n    // These need to be filtered against comp_locals before adding to referenced.\n    let mut inner_refs: AHashSet<String> = AHashSet::new();\n\n    for (i, comp) in generators.iter().enumerate() {\n        if i == 0 {\n            // FIRST generator's iter expression truly references enclosing scope\n            // (evaluated before any loop variable is defined).\n            collect_referenced_names_from_expr(&comp.iter, referenced, interner);\n        } else {\n            // SUBSEQUENT generators' iter expressions can reference prior loop variables.\n            // For example, in `[y for x in xs for y in x]`, the `x` in the second\n            // generator's iter is the first generator's loop variable, not outer scope.\n            collect_referenced_names_from_expr(&comp.iter, &mut inner_refs, interner);\n        }\n\n        // Add this generator's target(s) to local set\n        collect_names_from_unpack_target(&comp.target, &mut comp_locals, interner);\n\n        // Filter conditions can see prior loop variables - collect separately\n        for cond in &comp.ifs {\n            collect_referenced_names_from_expr(cond, &mut inner_refs, interner);\n        }\n    }\n\n    // Element expression(s) can see all loop variables - collect separately\n    if let Some(e) = elt {\n        collect_referenced_names_from_expr(e, &mut inner_refs, interner);\n    }\n    if let Some((k, v)) = key_value {\n        collect_referenced_names_from_expr(k, &mut inner_refs, interner);\n        collect_referenced_names_from_expr(v, &mut inner_refs, interner);\n    }\n\n    // Add inner references that are NOT comprehension-locals to the outer referenced set.\n    // Names that ARE comp_locals refer to the comprehension's loop variable, not enclosing scope.\n    for name in inner_refs {\n        if !comp_locals.contains(&name) {\n            referenced.insert(name);\n        }\n    }\n}\n\n/// Collects referenced names from argument expressions.\nfn collect_referenced_names_from_args(args: &ArgExprs, referenced: &mut AHashSet<String>, interner: &InternerBuilder) {\n    match args {\n        ArgExprs::Empty => {}\n        ArgExprs::One(e) => collect_referenced_names_from_expr(e, referenced, interner),\n        ArgExprs::Two(e1, e2) => {\n            collect_referenced_names_from_expr(e1, referenced, interner);\n            collect_referenced_names_from_expr(e2, referenced, interner);\n        }\n        ArgExprs::Args(exprs) => {\n            for e in exprs {\n                collect_referenced_names_from_expr(e, referenced, interner);\n            }\n        }\n        ArgExprs::Kwargs(kwargs) => {\n            for kwarg in kwargs {\n                collect_referenced_names_from_expr(&kwarg.value, referenced, interner);\n            }\n        }\n        ArgExprs::ArgsKargs {\n            args,\n            kwargs,\n            var_args,\n            var_kwargs,\n        } => {\n            if let Some(args) = args {\n                for e in args {\n                    collect_referenced_names_from_expr(e, referenced, interner);\n                }\n            }\n            if let Some(kwargs) = kwargs {\n                for kwarg in kwargs {\n                    collect_referenced_names_from_expr(&kwarg.value, referenced, interner);\n                }\n            }\n            if let Some(e) = var_args {\n                collect_referenced_names_from_expr(e, referenced, interner);\n            }\n            if let Some(e) = var_kwargs {\n                collect_referenced_names_from_expr(e, referenced, interner);\n            }\n        }\n        ArgExprs::GeneralizedCall { args, kwargs } => {\n            for arg in args {\n                match arg {\n                    CallArg::Value(e) | CallArg::Unpack(e) => {\n                        collect_referenced_names_from_expr(e, referenced, interner);\n                    }\n                }\n            }\n            for kwarg in kwargs {\n                match kwarg {\n                    CallKwarg::Named(kw) => {\n                        collect_referenced_names_from_expr(&kw.value, referenced, interner);\n                    }\n                    CallKwarg::Unpack(e) => {\n                        collect_referenced_names_from_expr(e, referenced, interner);\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Collects referenced names from f-string parts (both expressions and dynamic format specs).\nfn collect_referenced_names_from_fstring_parts(\n    parts: &[FStringPart],\n    referenced: &mut AHashSet<String>,\n    interner: &InternerBuilder,\n) {\n    for part in parts {\n        if let FStringPart::Interpolation { expr, format_spec, .. } = part {\n            collect_referenced_names_from_expr(expr, referenced, interner);\n            // Also check dynamic format specs which can contain interpolated expressions\n            if let Some(FormatSpec::Dynamic(spec_parts)) = format_spec {\n                collect_referenced_names_from_fstring_parts(spec_parts, referenced, interner);\n            }\n        }\n    }\n}\n\n/// Collects all names from an unpack target into the given set.\n///\n/// Recursively traverses nested tuples to find all identifier names.\nfn collect_names_from_unpack_target(target: &UnpackTarget, names: &mut AHashSet<String>, interner: &InternerBuilder) {\n    match target {\n        UnpackTarget::Name(ident) | UnpackTarget::Starred(ident) => {\n            names.insert(interner.get_str(ident.name_id).to_string());\n        }\n        UnpackTarget::Tuple { targets, .. } => {\n            for t in targets {\n                collect_names_from_unpack_target(t, names, interner);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/repl.rs",
    "content": "//! Stateful REPL execution support for Monty.\n//!\n//! This module implements incremental snippet execution where each new snippet\n//! is compiled and executed against persistent heap/namespace state without\n//! replaying previously executed snippets.\n\nuse std::mem;\n\nuse ahash::AHashMap;\nuse ruff_python_ast::token::TokenKind;\nuse ruff_python_parser::{InterpolatedStringErrorType, LexicalErrorType, ParseErrorType, parse_module};\n\nuse crate::{\n    ExcType, MontyException,\n    asyncio::CallId,\n    bytecode::{Code, Compiler, FrameExit, VM, VMSnapshot},\n    exception_private::{RunError, RunResult},\n    heap::{DropWithHeap, Heap},\n    intern::{InternerBuilder, Interns},\n    io::PrintWriter,\n    namespace::NamespaceId,\n    object::MontyObject,\n    os::OsFunction,\n    parse::parse_with_interner,\n    prepare::prepare_with_existing_names,\n    resource::ResourceTracker,\n    run_progress::{ConvertedExit, ExtFunctionResult, NameLookupResult, convert_frame_exit},\n    value::Value,\n};\n\n/// Stateful REPL session that executes snippets incrementally without replay.\n///\n/// `MontyRepl` preserves heap and global variable state between snippets.\n/// Each `feed()` compiles and executes only the new snippet against the current\n/// state, avoiding the cost and semantic risks of replaying prior code.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct MontyRepl<T: ResourceTracker> {\n    /// Script name used for runtime error messages and REPL identification.\n    ///\n    /// Incremental `feed()` / `start()` snippets intentionally use internal script names\n    /// like `<python-input-0>` to match CPython's interactive traceback style.\n    script_name: String,\n    /// Counter for generated `<python-input-N>` snippet filenames.\n    next_input_id: u64,\n    /// Stable mapping of global variable names to namespace slot IDs.\n    global_name_map: AHashMap<String, NamespaceId>,\n    /// Persistent intern table across snippets so intern/function IDs remain valid.\n    interns: Interns,\n    /// Persistent heap across snippets.\n    heap: Heap<T>,\n    /// Persistent global variable values across snippets.\n    ///\n    /// Indexed by `NamespaceId` slots from `global_name_map`. Between snippet\n    /// executions these are the only VM values that persist — stack and frames\n    /// are transient.\n    globals: Vec<Value>,\n}\n\nimpl<T: ResourceTracker> MontyRepl<T> {\n    /// Creates an empty REPL session with no code parsed or executed.\n    ///\n    /// All code execution is driven through `feed_run()` or `feed_start()`. This separates\n    /// construction from execution, matching the pattern used by `MontyRun::new()`.\n    #[must_use]\n    pub fn new(script_name: &str, resource_tracker: T) -> Self {\n        let heap = Heap::new(0, resource_tracker);\n\n        Self {\n            script_name: script_name.to_owned(),\n            next_input_id: 0,\n            global_name_map: AHashMap::new(),\n            interns: Interns::new(InternerBuilder::default(), Vec::new()),\n            heap,\n            globals: Vec::new(),\n        }\n    }\n\n    /// Starts executing a new snippet and returns suspendable REPL progress.\n    ///\n    /// This is the REPL equivalent of `MontyRun::start`: execution may complete,\n    /// suspend at external calls / OS calls / unresolved futures, or raise a Python\n    /// exception. Resume with the returned state object and eventually recover the\n    /// updated REPL from `ReplProgress::into_complete`.\n    ///\n    /// Unlike `MontyRepl::feed`, this method consumes `self` so runtime state can be\n    /// safely moved into snapshot objects for serialization and cross-process resume.\n    ///\n    /// On a Python-level runtime exception the REPL is **not** destroyed: it is\n    /// returned inside `ReplStartError` so the caller can continue feeding\n    /// subsequent snippets against the same heap and namespace state.\n    ///\n    /// # Errors\n    /// Returns `Err(Box<ReplStartError>)` for syntax, compile-time, or runtime\n    /// failures — the REPL session is always preserved inside the error.\n    pub fn feed_start(\n        self,\n        code: &str,\n        inputs: Vec<(String, MontyObject)>,\n        print: PrintWriter<'_>,\n    ) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        let mut this = self;\n        if code.is_empty() {\n            return Ok(ReplProgress::Complete {\n                repl: this,\n                value: MontyObject::None,\n            });\n        }\n\n        let (input_names, input_values): (Vec<_>, Vec<_>) = inputs.into_iter().unzip();\n\n        let input_script_name = this.next_input_script_name();\n        let executor = match ReplExecutor::new_repl_snippet(\n            code.to_owned(),\n            &input_script_name,\n            this.global_name_map.clone(),\n            &this.interns,\n            input_names,\n        ) {\n            Ok(exec) => exec,\n            Err(error) => return Err(Box::new(ReplStartError { repl: this, error })),\n        };\n\n        this.ensure_globals_size(executor.namespace_size);\n\n        let mut vm = VM::new(mem::take(&mut this.globals), &mut this.heap, &executor.interns, print);\n\n        // Inject inputs with VM alive\n        if let Err(error) = inject_inputs_into_vm(&executor, input_values, &mut vm) {\n            this.globals = vm.take_globals();\n            vm.cleanup();\n            return Err(Box::new(ReplStartError { repl: this, error }));\n        }\n\n        let vm_result = vm.run_module(&executor.module_code);\n\n        // Convert while VM alive, then snapshot or reclaim globals\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        if converted.needs_snapshot() {\n            let vm_state = vm.snapshot();\n            build_repl_progress(converted, Some(vm_state), executor, this)\n        } else {\n            this.globals = vm.take_globals();\n            vm.cleanup();\n            build_repl_progress(converted, None, executor, this)\n        }\n    }\n\n    /// Feeds and executes a new snippet against the current REPL state to completion.\n    ///\n    /// This compiles only `code` using the existing global slot map, extends the\n    /// global namespace if new names are introduced, and executes the snippet once.\n    /// Previously executed snippets are never replayed. If execution raises after\n    /// partially mutating globals, those mutations remain visible in later feeds,\n    /// matching Python REPL semantics.\n    ///\n    /// # Errors\n    /// Returns `MontyException` for syntax/compile/runtime failures.\n    pub fn feed_run(\n        &mut self,\n        code: &str,\n        inputs: Vec<(String, MontyObject)>,\n        print: PrintWriter<'_>,\n    ) -> Result<MontyObject, MontyException> {\n        if code.is_empty() {\n            return Ok(MontyObject::None);\n        }\n\n        let (input_names, input_values): (Vec<_>, Vec<_>) = inputs.into_iter().unzip();\n\n        let input_script_name = self.next_input_script_name();\n        let executor = ReplExecutor::new_repl_snippet(\n            code.to_owned(),\n            &input_script_name,\n            self.global_name_map.clone(),\n            &self.interns,\n            input_names,\n        )?;\n\n        self.ensure_globals_size(executor.namespace_size);\n\n        let mut vm = VM::new(mem::take(&mut self.globals), &mut self.heap, &executor.interns, print);\n\n        if let Err(e) = inject_inputs_into_vm(&executor, input_values, &mut vm) {\n            self.globals = vm.take_globals();\n            vm.cleanup();\n            return Err(e);\n        }\n\n        let mut frame_exit_result = vm.run_module(&executor.module_code);\n\n        // Handle NameLookup exits by raising NameError through the VM so that\n        // traceback information is properly captured. In the non-iterative REPL path,\n        // there's no host to resolve names, so all NameLookup exits become NameErrors.\n        while let Ok(FrameExit::NameLookup { name_id, .. }) = &frame_exit_result {\n            let name = executor.interns.get_str(*name_id);\n            let err = ExcType::name_error(name);\n            frame_exit_result = vm.resume_with_exception(err.into());\n        }\n\n        // Convert output while VM alive\n        let result = frame_exit_to_object(frame_exit_result, &mut vm);\n\n        // Reclaim globals before cleanup.\n        self.globals = vm.take_globals();\n        vm.cleanup();\n\n        // Commit compiler metadata even on runtime errors.\n        // Snippets can mutate globals before raising, and those values may contain\n        // FunctionId/StringId values that must be interpreted with the updated tables.\n        let ReplExecutor {\n            name_map,\n            interns,\n            code,\n            ..\n        } = executor;\n        self.global_name_map = name_map;\n        self.interns = interns;\n\n        result.map_err(|e| e.into_python_exception(&self.interns, &code))\n    }\n\n    /// Grows the globals vector to at least `size` slots.\n    ///\n    /// Newly introduced slots are initialized to `Undefined` to keep slot alignment\n    /// with the compiler's global-name map.\n    fn ensure_globals_size(&mut self, size: usize) {\n        if self.globals.len() < size {\n            self.globals.resize_with(size, || Value::Undefined);\n        }\n    }\n\n    /// Returns the generated filename for the next interactive snippet.\n    ///\n    /// CPython labels interactive snippets as `<python-input-N>` and increments\n    /// N for each feed attempt. Matching this improves traceback ergonomics and\n    /// makes REPL errors easier to correlate with user input history.\n    fn next_input_script_name(&mut self) -> String {\n        let input_id = self.next_input_id;\n        self.next_input_id += 1;\n        format!(\"<python-input-{input_id}>\")\n    }\n}\n\nimpl<T: ResourceTracker + serde::Serialize> MontyRepl<T> {\n    /// Serializes the REPL session state to bytes.\n    ///\n    /// This includes heap + globals + global slot mapping, allowing snapshot/restore\n    /// of interactive state between process runs.\n    ///\n    /// # Errors\n    /// Returns an error if serialization fails.\n    pub fn dump(&self) -> Result<Vec<u8>, postcard::Error> {\n        postcard::to_allocvec(self)\n    }\n}\n\nimpl<T: ResourceTracker + serde::de::DeserializeOwned> MontyRepl<T> {\n    /// Restores a REPL session from bytes produced by `MontyRepl::dump`.\n    ///\n    /// # Errors\n    /// Returns an error if deserialization fails.\n    pub fn load(bytes: &[u8]) -> Result<Self, postcard::Error> {\n        postcard::from_bytes(bytes)\n    }\n}\n\nimpl<T: ResourceTracker> Drop for MontyRepl<T> {\n    fn drop(&mut self) {\n        self.globals.drain(..).drop_with_heap(&mut self.heap);\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplProgress and per-variant structs\n// ---------------------------------------------------------------------------\n\n/// Result of a single suspendable REPL snippet execution.\n///\n/// This mirrors `RunProgress` but returns the updated `MontyRepl` on completion\n/// so callers can continue feeding additional snippets without replaying prior code.\n/// Each variant (except `Complete`) wraps a dedicated struct with only the relevant\n/// resume methods.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub enum ReplProgress<T: ResourceTracker> {\n    /// Execution paused at an external function call or dataclass method call.\n    FunctionCall(ReplFunctionCall<T>),\n    /// Execution paused for an OS-level operation.\n    OsCall(ReplOsCall<T>),\n    /// All async tasks are blocked waiting for external futures to resolve.\n    ResolveFutures(ReplResolveFutures<T>),\n    /// Execution paused for an unresolved name lookup.\n    NameLookup(ReplNameLookup<T>),\n    /// Snippet execution completed with the updated REPL and result value.\n    Complete {\n        /// Updated REPL session state to continue feeding snippets.\n        repl: MontyRepl<T>,\n        /// Final result produced by the snippet.\n        value: MontyObject,\n    },\n}\n\n/// Error returned when a REPL snippet raises a Python exception during `start()` or `resume()`.\n///\n/// Unlike syntax/compile errors which consume the REPL, runtime errors preserve\n/// the full session state so the caller can inspect the error and continue feeding\n/// subsequent snippets. Any global mutations that occurred before the exception\n/// remain visible in the returned `repl`.\n#[derive(Debug)]\npub struct ReplStartError<T: ResourceTracker> {\n    /// REPL session state after the failed snippet — ready for further use.\n    pub repl: MontyRepl<T>,\n    /// The Python exception that was raised.\n    pub error: MontyException,\n}\n\nimpl<T: ResourceTracker> ReplProgress<T> {\n    /// Consumes the progress and returns the `ReplFunctionCall` struct.\n    #[must_use]\n    pub fn into_function_call(self) -> Option<ReplFunctionCall<T>> {\n        match self {\n            Self::FunctionCall(call) => Some(call),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the `ReplResolveFutures` struct.\n    #[must_use]\n    pub fn into_resolve_futures(self) -> Option<ReplResolveFutures<T>> {\n        match self {\n            Self::ResolveFutures(state) => Some(state),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the `ReplNameLookup` struct.\n    #[must_use]\n    pub fn into_name_lookup(self) -> Option<ReplNameLookup<T>> {\n        match self {\n            Self::NameLookup(lookup) => Some(lookup),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the completed REPL and value.\n    #[must_use]\n    pub fn into_complete(self) -> Option<(MontyRepl<T>, MontyObject)> {\n        match self {\n            Self::Complete { repl, value } => Some((repl, value)),\n            _ => None,\n        }\n    }\n\n    /// Extracts the REPL session from any progress variant, discarding\n    /// the in-flight execution state.\n    ///\n    /// Use this to recover the REPL when you need to abandon the current\n    /// snippet (e.g. because `feed_run` doesn't support async futures).\n    /// The REPL state reflects any mutations that occurred before the\n    /// snapshot was taken.\n    #[must_use]\n    pub fn into_repl(self) -> MontyRepl<T> {\n        match self {\n            Self::FunctionCall(call) => call.into_repl(),\n            Self::OsCall(call) => call.into_repl(),\n            Self::ResolveFutures(state) => state.into_repl(),\n            Self::NameLookup(lookup) => lookup.into_repl(),\n            Self::Complete { repl, .. } => repl,\n        }\n    }\n}\n\nimpl<T: ResourceTracker + serde::Serialize> ReplProgress<T> {\n    /// Serializes the REPL execution progress to a binary format.\n    ///\n    /// # Errors\n    /// Returns an error if serialization fails.\n    pub fn dump(&self) -> Result<Vec<u8>, postcard::Error> {\n        postcard::to_allocvec(self)\n    }\n}\n\nimpl<T: ResourceTracker + serde::de::DeserializeOwned> ReplProgress<T> {\n    /// Deserializes REPL execution progress from a binary format.\n    ///\n    /// # Errors\n    /// Returns an error if deserialization fails.\n    pub fn load(bytes: &[u8]) -> Result<Self, postcard::Error> {\n        postcard::from_bytes(bytes)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplFunctionCall\n// ---------------------------------------------------------------------------\n\n/// REPL execution paused at an external function call or dataclass method call.\n///\n/// Resume with `resume(result, print)` to provide the return value and continue,\n/// or `resume_pending(print)` to push an `ExternalFuture` for async resolution.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct ReplFunctionCall<T: ResourceTracker> {\n    /// The name of the function or method being called.\n    pub function_name: String,\n    /// The positional arguments passed to the function.\n    pub args: Vec<MontyObject>,\n    /// The keyword arguments passed to the function (key, value pairs).\n    pub kwargs: Vec<(MontyObject, MontyObject)>,\n    /// Unique identifier for this call (used for async correlation).\n    pub call_id: u32,\n    /// Whether this is a dataclass method call (first arg is `self`).\n    pub method_call: bool,\n    /// Internal REPL execution snapshot.\n    snapshot: ReplSnapshot<T>,\n}\n\nimpl<T: ResourceTracker> ReplFunctionCall<T> {\n    /// Extracts the REPL session, discarding the in-flight execution state.\n    ///\n    /// Restores globals from the VM snapshot so the REPL remains usable.\n    #[must_use]\n    pub fn into_repl(self) -> MontyRepl<T> {\n        self.snapshot.into_repl()\n    }\n\n    /// Resumes snippet execution with an external result.\n    pub fn resume(\n        self,\n        result: impl Into<ExtFunctionResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        self.snapshot.run(result, print)\n    }\n\n    /// Resumes execution by pushing an `ExternalFuture` for async resolution.\n    ///\n    /// Uses `self.call_id` internally — no need to pass it again.\n    pub fn resume_pending(self, print: PrintWriter<'_>) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        self.snapshot.run(ExtFunctionResult::Future(self.call_id), print)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplOsCall\n// ---------------------------------------------------------------------------\n\n/// REPL execution paused for an OS-level operation.\n///\n/// Resume with `resume(result, print)` to provide the OS call result and continue.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct ReplOsCall<T: ResourceTracker> {\n    /// The OS function to execute.\n    pub function: OsFunction,\n    /// The positional arguments for the OS function.\n    pub args: Vec<MontyObject>,\n    /// The keyword arguments passed to the function (key, value pairs).\n    pub kwargs: Vec<(MontyObject, MontyObject)>,\n    /// Unique identifier for this call (used for async correlation).\n    pub call_id: u32,\n    /// Internal REPL execution snapshot.\n    snapshot: ReplSnapshot<T>,\n}\n\nimpl<T: ResourceTracker> ReplOsCall<T> {\n    /// Extracts the REPL session, discarding the in-flight execution state.\n    ///\n    /// Restores globals from the VM snapshot so the REPL remains usable.\n    #[must_use]\n    pub fn into_repl(self) -> MontyRepl<T> {\n        self.snapshot.into_repl()\n    }\n\n    /// Resumes snippet execution with the OS call result.\n    pub fn resume(\n        self,\n        result: impl Into<ExtFunctionResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        self.snapshot.run(result, print)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplNameLookup\n// ---------------------------------------------------------------------------\n\n/// REPL execution paused for an unresolved name lookup.\n///\n/// The host should check if the name corresponds to a known external function or\n/// value. Call `resume(result, print)` with the appropriate `NameLookupResult`.\n/// The namespace slot and scope are managed internally.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct ReplNameLookup<T: ResourceTracker> {\n    /// The name being looked up.\n    pub name: String,\n    /// The namespace slot where the resolved value should be cached.\n    namespace_slot: u16,\n    /// Whether this is a global slot or a local/function slot.\n    is_global: bool,\n    /// Internal REPL execution snapshot.\n    snapshot: ReplSnapshot<T>,\n}\n\nimpl<T: ResourceTracker> ReplNameLookup<T> {\n    /// Extracts the REPL session, discarding the in-flight execution state.\n    ///\n    /// Restores globals from the VM snapshot so the REPL remains usable.\n    #[must_use]\n    pub fn into_repl(self) -> MontyRepl<T> {\n        self.snapshot.into_repl()\n    }\n\n    /// Resumes execution after name resolution.\n    ///\n    /// Caches the resolved value in the namespace slot before restoring the VM,\n    /// then either pushes the value onto the stack or raises `NameError`.\n    pub fn resume(\n        self,\n        result: NameLookupResult,\n        print: PrintWriter<'_>,\n    ) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        let Self {\n            name,\n            namespace_slot,\n            is_global,\n            snapshot,\n        } = self;\n\n        let ReplSnapshot {\n            mut repl,\n            executor,\n            vm_state,\n        } = snapshot;\n\n        // Restore the VM first, then convert inside its lifetime\n        let mut vm = VM::restore(\n            vm_state,\n            &executor.module_code,\n            &mut repl.heap,\n            &executor.interns,\n            print,\n        );\n\n        // Resolve the name lookup result with the VM alive\n        let vm_result = match result {\n            NameLookupResult::Value(obj) => {\n                let value = match obj.to_value(&mut vm) {\n                    Ok(v) => v,\n                    Err(e) => {\n                        repl.globals = vm.take_globals();\n                        vm.cleanup();\n                        let error = MontyException::runtime_error(format!(\"invalid name lookup result: {e}\"));\n                        return Err(Box::new(ReplStartError { repl, error }));\n                    }\n                };\n\n                // Cache the resolved value in the appropriate slot\n                let slot = namespace_slot as usize;\n                if is_global {\n                    let cloned = value.clone_with_heap(vm.heap);\n                    let old = mem::replace(&mut vm.globals[slot], cloned);\n                    old.drop_with_heap(vm.heap);\n                } else {\n                    let stack_base = vm.current_stack_base();\n                    let cloned = value.clone_with_heap(vm.heap);\n                    let old = mem::replace(&mut vm.stack[stack_base + slot], cloned);\n                    old.drop_with_heap(vm.heap);\n                }\n\n                vm.push(value);\n                vm.run()\n            }\n            NameLookupResult::Undefined => {\n                let err: RunError = ExcType::name_error(&name).into();\n                vm.resume_with_exception(err)\n            }\n        };\n\n        // Convert while VM alive, then snapshot or reclaim globals\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        if converted.needs_snapshot() {\n            let vm_state = vm.snapshot();\n            build_repl_progress(converted, Some(vm_state), executor, repl)\n        } else {\n            repl.globals = vm.take_globals();\n            vm.cleanup();\n            build_repl_progress(converted, None, executor, repl)\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplResolveFutures\n// ---------------------------------------------------------------------------\n\n/// REPL execution state blocked on unresolved external futures.\n///\n/// This is the REPL-aware counterpart to `ResolveFutures`.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct ReplResolveFutures<T: ResourceTracker> {\n    /// Persistent REPL session state while this snippet is suspended.\n    repl: MontyRepl<T>,\n    /// Compiled snippet and intern/function tables for this execution.\n    executor: ReplExecutor,\n    /// VM stack/frame state at suspension.\n    vm_state: VMSnapshot,\n    /// Pending call IDs expected by this snapshot.\n    pending_call_ids: Vec<u32>,\n}\n\nimpl<T: ResourceTracker> ReplResolveFutures<T> {\n    /// Extracts the REPL session, discarding the in-flight execution state.\n    #[must_use]\n    pub fn into_repl(self) -> MontyRepl<T> {\n        self.repl\n    }\n\n    /// Returns unresolved call IDs for this suspended state.\n    #[must_use]\n    pub fn pending_call_ids(&self) -> &[u32] {\n        &self.pending_call_ids\n    }\n\n    /// Resumes snippet execution with zero or more resolved futures.\n    ///\n    /// Supports incremental resolution: callers can provide only a subset of\n    /// pending call IDs and continue resolving over multiple resumes.\n    ///\n    /// All errors — including API misuse (unknown `call_id`) and Python-level\n    /// runtime failures — are returned as `Err(Box<ReplStartError>)` so the REPL\n    /// session is always preserved.\n    pub fn resume(\n        self,\n        results: Vec<(u32, ExtFunctionResult)>,\n        print: PrintWriter<'_>,\n    ) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        let Self {\n            mut repl,\n            executor,\n            vm_state,\n            pending_call_ids,\n        } = self;\n\n        let invalid_call_id = results\n            .iter()\n            .find(|(call_id, _)| !pending_call_ids.contains(call_id))\n            .map(|(call_id, _)| *call_id);\n\n        let mut vm = VM::restore(\n            vm_state,\n            &executor.module_code,\n            &mut repl.heap,\n            &executor.interns,\n            print,\n        );\n\n        if let Some(call_id) = invalid_call_id {\n            repl.globals = vm.take_globals();\n            vm.cleanup();\n            let error = MontyException::runtime_error(format!(\n                \"unknown call_id {call_id}, expected one of: {pending_call_ids:?}\"\n            ));\n            return Err(Box::new(ReplStartError { repl, error }));\n        }\n\n        for (call_id, ext_result) in results {\n            match ext_result {\n                ExtFunctionResult::Return(obj) => {\n                    if let Err(e) = vm.resolve_future(call_id, obj) {\n                        repl.globals = vm.take_globals();\n                        vm.cleanup();\n                        let error =\n                            MontyException::runtime_error(format!(\"Invalid return type for call {call_id}: {e}\"));\n                        return Err(Box::new(ReplStartError { repl, error }));\n                    }\n                }\n                ExtFunctionResult::Error(exc) => vm.fail_future(call_id, RunError::from(exc)),\n                ExtFunctionResult::Future(_) => {}\n                ExtFunctionResult::NotFound(function_name) => {\n                    vm.fail_future(call_id, ExtFunctionResult::not_found_exc(&function_name));\n                }\n            }\n        }\n\n        if let Some(error) = vm.take_failed_task_error() {\n            repl.globals = vm.take_globals();\n            vm.cleanup();\n            let error = error.into_python_exception(&executor.interns, &executor.code);\n            return Err(Box::new(ReplStartError { repl, error }));\n        }\n\n        let main_task_ready = vm.prepare_current_task_after_resolve();\n\n        let loaded_task = match vm.load_ready_task_if_needed() {\n            Ok(loaded) => loaded,\n            Err(e) => {\n                repl.globals = vm.take_globals();\n                vm.cleanup();\n                let error = e.into_python_exception(&executor.interns, &executor.code);\n                return Err(Box::new(ReplStartError { repl, error }));\n            }\n        };\n\n        if !main_task_ready && !loaded_task {\n            let pending_call_ids = vm.get_pending_call_ids();\n            if !pending_call_ids.is_empty() {\n                let vm_state = vm.snapshot();\n                let pending_call_ids: Vec<u32> = pending_call_ids.iter().map(|id| id.raw()).collect();\n                return Ok(ReplProgress::ResolveFutures(Self {\n                    repl,\n                    executor,\n                    vm_state,\n                    pending_call_ids,\n                }));\n            }\n        }\n\n        let vm_result = vm.run();\n\n        // Convert while VM alive, then snapshot or reclaim globals\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        if converted.needs_snapshot() {\n            let vm_state = vm.snapshot();\n            build_repl_progress(converted, Some(vm_state), executor, repl)\n        } else {\n            repl.globals = vm.take_globals();\n            vm.cleanup();\n            build_repl_progress(converted, None, executor, repl)\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplContinuationMode — public utility for interactive input collection\n// ---------------------------------------------------------------------------\n\n/// Parse-derived continuation state for interactive REPL input collection.\n///\n/// `monty-cli` uses this to decide whether to execute the buffered snippet\n/// immediately, keep collecting continuation lines, or require a terminating\n/// blank line for block statements (`if:`, `def:`, etc.).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ReplContinuationMode {\n    /// The current snippet is syntactically complete and can run now.\n    Complete,\n    /// The snippet is incomplete and needs more continuation lines.\n    IncompleteImplicit,\n    /// The snippet opened an indented block and should wait for a trailing blank\n    /// line before execution, matching CPython interactive behavior.\n    IncompleteBlock,\n}\n\n/// Detects whether REPL source is complete or needs more input.\n///\n/// This mirrors CPython's broad interactive behavior:\n/// - Incomplete bracketed / parenthesized / triple-quoted constructs continue.\n/// - Clause headers (`if:`, `def:`, etc.) require an indented body and then a\n///   terminating blank line before execution.\n/// - All other parse outcomes are treated as complete (either valid code or a\n///   syntax error that should be shown immediately).\n#[must_use]\npub fn detect_repl_continuation_mode(source: &str) -> ReplContinuationMode {\n    let Err(error) = parse_module(source) else {\n        return ReplContinuationMode::Complete;\n    };\n\n    match error.error {\n        ParseErrorType::OtherError(msg) => {\n            if msg.starts_with(\"Expected an indented block after \") {\n                ReplContinuationMode::IncompleteBlock\n            } else {\n                ReplContinuationMode::Complete\n            }\n        }\n        ParseErrorType::Lexical(LexicalErrorType::Eof)\n        | ParseErrorType::ExpectedToken {\n            found: TokenKind::EndOfFile,\n            ..\n        }\n        | ParseErrorType::FStringError(InterpolatedStringErrorType::UnterminatedTripleQuotedString)\n        | ParseErrorType::TStringError(InterpolatedStringErrorType::UnterminatedTripleQuotedString) => {\n            ReplContinuationMode::IncompleteImplicit\n        }\n        _ => ReplContinuationMode::Complete,\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplExecutor — internal compilation helper\n// ---------------------------------------------------------------------------\n\n/// Compiled snippet representation used only by REPL execution.\n///\n/// This intentionally mirrors the data shape needed by VM execution in\n/// `run.rs` but lives in the REPL module so REPL evolution does not require\n/// changing `run.rs`.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nstruct ReplExecutor {\n    /// Number of slots needed in the global namespace.\n    namespace_size: usize,\n    /// Maps variable names to their indices in the namespace.\n    ///\n    /// Stable slot assignment is required across snippets so previously created\n    /// objects continue to resolve names correctly.\n    name_map: AHashMap<String, NamespaceId>,\n    /// Compiled bytecode for the snippet.\n    module_code: Code,\n    /// Interned strings and compiled functions for this snippet.\n    interns: Interns,\n    /// Source code used for traceback/error rendering.\n    code: String,\n    /// Input variable names that were injected for this snippet.\n    ///\n    /// Stored so that `inject_inputs` can look up their namespace slots\n    /// after compilation assigns them.\n    input_names: Vec<String>,\n}\n\nimpl ReplExecutor {\n    /// Compiles one REPL snippet against existing session metadata.\n    ///\n    /// This differs from normal compilation in three ways required for true\n    /// no-replay execution:\n    /// - Seeds parsing from `existing_interns` so old `StringId` values stay stable.\n    /// - Seeds compilation with existing functions so old `FunctionId` values remain valid.\n    /// - Reuses `existing_name_map` and appends new global names only.\n    ///\n    /// `input_names` are pre-registered in the name map before preparation so they\n    /// receive stable namespace slots that `inject_inputs` can use to store values.\n    fn new_repl_snippet(\n        code: String,\n        script_name: &str,\n        mut existing_name_map: AHashMap<String, NamespaceId>,\n        existing_interns: &Interns,\n        input_names: Vec<String>,\n    ) -> Result<Self, MontyException> {\n        // Pre-register input names so they get stable slots before preparation.\n        for name in &input_names {\n            let next_slot = existing_name_map.len();\n            existing_name_map\n                .entry(name.clone())\n                .or_insert_with(|| NamespaceId::new(next_slot));\n        }\n\n        let seeded_interner = InternerBuilder::from_interns(existing_interns, &code);\n        let parse_result = parse_with_interner(&code, script_name, seeded_interner)\n            .map_err(|e| e.into_python_exc(script_name, &code))?;\n        let prepared = prepare_with_existing_names(parse_result, existing_name_map)\n            .map_err(|e| e.into_python_exc(script_name, &code))?;\n\n        let existing_functions = existing_interns.functions_clone();\n        let mut interns = Interns::new(prepared.interner, Vec::new());\n        let namespace_size_u16 = u16::try_from(prepared.namespace_size).expect(\"module namespace size exceeds u16\");\n        let compile_result =\n            Compiler::compile_module_with_functions(&prepared.nodes, &interns, namespace_size_u16, existing_functions)\n                .map_err(|e| e.into_python_exc(script_name, &code))?;\n        interns.set_functions(compile_result.functions);\n\n        Ok(Self {\n            namespace_size: prepared.namespace_size,\n            name_map: prepared.name_map,\n            module_code: compile_result.code,\n            interns,\n            code,\n            input_names,\n        })\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ReplSnapshot — internal execution state for suspend/resume\n// ---------------------------------------------------------------------------\n\n/// REPL execution state that can be resumed after an external call.\n///\n/// This is the REPL-aware counterpart to `Snapshot`. It is `pub(crate)` —\n/// callers interact with the per-variant structs (`ReplFunctionCall`, etc.).\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub(crate) struct ReplSnapshot<T: ResourceTracker> {\n    /// Persistent REPL session state while this snippet is suspended.\n    repl: MontyRepl<T>,\n    /// Compiled snippet and intern/function tables for this execution.\n    executor: ReplExecutor,\n    /// VM stack/frame state at suspension.\n    vm_state: VMSnapshot,\n}\n\nimpl<T: ResourceTracker> ReplSnapshot<T> {\n    /// Extracts the REPL session, restoring globals from the VM snapshot.\n    ///\n    /// When a snapshot is taken, globals live inside the `VMSnapshot`.\n    /// This method creates an empty snapshot from just the globals so the REPL\n    /// can be used for further snippets.\n    fn into_repl(self) -> MontyRepl<T> {\n        let Self { mut repl, vm_state, .. } = self;\n        repl.globals = vm_state.globals;\n        repl\n    }\n\n    /// Continues snippet execution with an external result.\n    fn run(\n        self,\n        result: impl Into<ExtFunctionResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n        let Self {\n            mut repl,\n            executor,\n            vm_state,\n        } = self;\n\n        let ext_result = result.into();\n\n        let mut vm = VM::restore(\n            vm_state,\n            &executor.module_code,\n            &mut repl.heap,\n            &executor.interns,\n            print,\n        );\n\n        let vm_result = match ext_result {\n            ExtFunctionResult::Return(obj) => vm.resume(obj),\n            ExtFunctionResult::Error(exc) => vm.resume_with_exception(exc.into()),\n            ExtFunctionResult::Future(raw_call_id) => {\n                let call_id = CallId::new(raw_call_id);\n                vm.add_pending_call(call_id);\n                vm.push(Value::ExternalFuture(call_id));\n                vm.run()\n            }\n            ExtFunctionResult::NotFound(function_name) => {\n                vm.resume_with_exception(ExtFunctionResult::not_found_exc(&function_name))\n            }\n        };\n\n        // Convert while VM alive, then snapshot or reclaim globals\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        if converted.needs_snapshot() {\n            let vm_state = vm.snapshot();\n            build_repl_progress(converted, Some(vm_state), executor, repl)\n        } else {\n            repl.globals = vm.take_globals();\n            vm.cleanup();\n            build_repl_progress(converted, None, executor, repl)\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Private helper functions\n// ---------------------------------------------------------------------------\n\n/// Injects input values into the VM's global namespace slots.\n///\n/// Converts each `MontyObject` to a `Value` while the VM is alive, then stores\n/// it in the global slot that the compiler assigned for the corresponding input name.\nfn inject_inputs_into_vm(\n    executor: &ReplExecutor,\n    input_values: Vec<MontyObject>,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<(), MontyException> {\n    for (name, obj) in executor.input_names.iter().zip(input_values) {\n        let slot = executor\n            .name_map\n            .get(name)\n            .expect(\"input name should have a namespace slot\")\n            .index();\n        let value = obj\n            .to_value(vm)\n            .map_err(|e| MontyException::runtime_error(format!(\"invalid input type: {e}\")))?;\n        let old = mem::replace(&mut vm.globals[slot], value);\n        old.drop_with_heap(vm.heap);\n    }\n    Ok(())\n}\n\n/// Converts module/frame exit results into plain `MontyObject` outputs.\n///\n/// Used by the non-iterative `feed_run` path where suspendable outcomes (external calls,\n/// name lookups) are not supported and should produce errors.\nfn frame_exit_to_object(\n    frame_exit_result: RunResult<FrameExit>,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<MontyObject> {\n    match frame_exit_result? {\n        FrameExit::Return(return_value) => Ok(MontyObject::new(return_value, vm)),\n        FrameExit::ExternalCall {\n            function_name, args, ..\n        } => {\n            args.drop_with_heap(vm.heap);\n            let function_name = function_name.as_str(vm.interns);\n            Err(ExcType::not_implemented(format!(\n                \"External function '{function_name}' not implemented with standard execution\"\n            ))\n            .into())\n        }\n        FrameExit::OsCall { function, args, .. } => {\n            args.drop_with_heap(vm.heap);\n            Err(ExcType::not_implemented(format!(\n                \"OS function '{function}' not implemented with standard execution\"\n            ))\n            .into())\n        }\n        FrameExit::MethodCall { method_name, args, .. } => {\n            args.drop_with_heap(vm.heap);\n            let name = method_name.as_str(vm.interns);\n            Err(\n                ExcType::not_implemented(format!(\"Method call '{name}' not implemented with standard execution\"))\n                    .into(),\n            )\n        }\n        FrameExit::ResolveFutures(_) => {\n            Err(ExcType::not_implemented(\"async futures not supported by standard execution.\").into())\n        }\n        FrameExit::NameLookup { name_id, .. } => {\n            let name = vm.interns.get_str(name_id);\n            Err(ExcType::name_error(name).into())\n        }\n    }\n}\n\n/// Assembles a `ReplProgress` from already-converted data.\n///\n/// This is the REPL equivalent of `build_run_progress`. On completion/error,\n/// compiler metadata is committed to the REPL so subsequent snippets see\n/// updated intern tables and name maps.\nfn build_repl_progress<T: ResourceTracker>(\n    converted: ConvertedExit,\n    vm_state: Option<VMSnapshot>,\n    executor: ReplExecutor,\n    mut repl: MontyRepl<T>,\n) -> Result<ReplProgress<T>, Box<ReplStartError<T>>> {\n    macro_rules! new_repl_snapshot {\n        () => {\n            ReplSnapshot {\n                repl,\n                executor,\n                vm_state: vm_state.expect(\"snapshot should exist\"),\n            }\n        };\n    }\n\n    match converted {\n        ConvertedExit::Complete(obj) => {\n            let ReplExecutor { name_map, interns, .. } = executor;\n            repl.global_name_map = name_map;\n            repl.interns = interns;\n            Ok(ReplProgress::Complete { repl, value: obj })\n        }\n        ConvertedExit::FunctionCall {\n            function_name,\n            args,\n            kwargs,\n            call_id,\n            method_call,\n        } => Ok(ReplProgress::FunctionCall(ReplFunctionCall {\n            function_name,\n            args,\n            kwargs,\n            call_id,\n            method_call,\n            snapshot: new_repl_snapshot!(),\n        })),\n        ConvertedExit::OsCall {\n            function,\n            args,\n            kwargs,\n            call_id,\n        } => Ok(ReplProgress::OsCall(ReplOsCall {\n            function,\n            args,\n            kwargs,\n            call_id,\n            snapshot: new_repl_snapshot!(),\n        })),\n        ConvertedExit::ResolveFutures(pending_call_ids) => Ok(ReplProgress::ResolveFutures(ReplResolveFutures {\n            repl,\n            executor,\n            vm_state: vm_state.expect(\"snapshot should exist for ResolveFutures\"),\n            pending_call_ids,\n        })),\n        ConvertedExit::NameLookup {\n            name,\n            namespace_slot,\n            is_global,\n        } => Ok(ReplProgress::NameLookup(ReplNameLookup {\n            name,\n            namespace_slot,\n            is_global,\n            snapshot: new_repl_snapshot!(),\n        })),\n        ConvertedExit::Error(err) => {\n            let error = err.into_python_exception(&executor.interns, &executor.code);\n            // Commit compiler metadata even on runtime errors, matching feed() behavior.\n            // Snippets can create new variables or functions before raising, and those\n            // values may reference FunctionId/StringId values from the new tables.\n            let ReplExecutor { name_map, interns, .. } = executor;\n            repl.global_name_map = name_map;\n            repl.interns = interns;\n            Err(Box::new(ReplStartError { repl, error }))\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/resource.rs",
    "content": "use std::{\n    fmt,\n    sync::atomic::{AtomicU16, Ordering},\n    time::{Duration, Instant},\n};\n\nuse crate::{\n    ExcType, MontyException,\n    exception_private::{ExceptionRaise, RawStackFrame, RunError, SimpleException},\n};\n\n/// Threshold in bytes above which `check_large_result` is called.\n///\n/// Operations that may produce results larger than this threshold (100KB) should call\n/// `check_large_result` before performing the operation. This prevents DoS attacks\n/// where operations like `2 ** 10_000_000` allocate huge amounts of memory before\n/// the allocation check can catch them.\npub const LARGE_RESULT_THRESHOLD: usize = 100_000;\n\n/// Pre-checks that an operation producing `item_len * count` bytes won't exceed resource limits.\n///\n/// Used for sequence repeats (`'x' * 999_999_999`), padding operations\n/// (`str.ljust`, `str.center`, `str.zfill`, etc.), and any other operation\n/// where the result size is a simple product of two known values.\npub fn check_repeat_size(item_len: usize, count: usize, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {\n    check_estimated_size(item_len.saturating_mul(count), tracker)\n}\n\n/// Pre-checks that `base ** exponent` won't exceed resource limits before computing.\n///\n/// The result of `base ** exp` has approximately `base_bits * exp` bits.\n/// For bases with 0 or 1 significant bits (0, 1, -1), the result is always\n/// small regardless of exponent, so the check is skipped.\n///\n/// The estimate includes a 4× safety multiplier because `BigInt::pow` uses repeated squaring,\n/// which allocates intermediate values on the Rust heap (not tracked by the resource tracker).\n/// At peak, old/new base and old/new accumulator coexist simultaneously during each\n/// multiplication step, requiring roughly 4× the final result size in memory.\npub fn check_pow_size(base_bits: u64, exponent: u64, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {\n    // 0**n = 0, 1**n = 1, (-1)**n = ±1 — always small\n    if base_bits <= 1 {\n        return Ok(());\n    }\n    let result_bytes = estimate_bits_to_bytes(base_bits.saturating_mul(exponent));\n    // Repeated squaring needs ~4× result size in peak memory (old/new base + old/new accumulator\n    // coexist during each multiplication step), and these are Rust heap allocations not tracked\n    // by the resource tracker.\n    check_estimated_size(result_bytes.saturating_mul(4), tracker)\n}\n\n/// Pre-checks that an integer multiplication won't exceed resource limits.\n///\n/// The result of multiplying two numbers has at most `a_bits + b_bits` bits.\npub fn check_mult_size(a_bits: u64, b_bits: u64, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {\n    check_estimated_size(estimate_bits_to_bytes(a_bits.saturating_add(b_bits)), tracker)\n}\n\n/// Pre-checks that a left shift won't exceed resource limits.\n///\n/// The result of `value << shift` has approximately `value_bits + shift` bits.\n/// For zero values the result is always zero, so the check is skipped.\npub fn check_lshift_size(\n    value_bits: u64,\n    shift_amount: u64,\n    tracker: &impl ResourceTracker,\n) -> Result<(), ResourceError> {\n    if value_bits == 0 {\n        return Ok(());\n    }\n    check_estimated_size(estimate_bits_to_bytes(value_bits.saturating_add(shift_amount)), tracker)\n}\n\n/// Pre-checks that an integer division overflow promotion won't exceed resource limits.\n///\n/// Division results are bounded by the dividend size, but we still check for consistency\n/// with other BigInt promotion paths.\npub fn check_div_size(dividend_bits: u64, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {\n    check_estimated_size(estimate_bits_to_bytes(dividend_bits), tracker)\n}\n\n/// Pre-checks that a string/bytes replace won't exceed resource limits before allocating.\n///\n/// This prevents DoS via expressions like `('a' * 1000).replace('a', 'b' * 10_000_000)`\n/// where a small tracked input is amplified into a huge untracked Rust `String`/`Vec`\n/// by `String::replace()` before `allocate_string()` can check the result.\n///\n/// The upper bound on result size is: if `old` is non-empty, at most `input_len / old_len`\n/// replacements can occur, each producing `new_len` bytes instead of `old_len`. When `count`\n/// is specified, replacements are capped to that value.\npub fn check_replace_size(\n    input_len: usize,\n    old_len: usize,\n    new_len: usize,\n    count: i64,\n    tracker: &impl ResourceTracker,\n) -> Result<(), ResourceError> {\n    // Empty pattern (old_len == 0): inserts before each element + after the last = input_len + 1\n    let max_replacements = input_len\n        .checked_div(old_len)\n        .unwrap_or_else(|| input_len.saturating_add(1));\n\n    let replacements = if count < 0 {\n        max_replacements\n    } else {\n        max_replacements.min(usize::try_from(count).unwrap_or(usize::MAX))\n    };\n\n    // Result = input_len - (replacements * old_len) + (replacements * new_len)\n    let removed = replacements.saturating_mul(old_len);\n    let added = replacements.saturating_mul(new_len);\n    let estimated = input_len.saturating_sub(removed).saturating_add(added);\n\n    check_estimated_size(estimated, tracker)\n}\n\n/// Checks an estimated result size against the resource tracker.\n///\n/// Only calls the tracker when the estimate exceeds `LARGE_RESULT_THRESHOLD`\n/// to avoid overhead on small operations.\npub(crate) fn check_estimated_size(\n    estimated_bytes: usize,\n    tracker: &impl ResourceTracker,\n) -> Result<(), ResourceError> {\n    if estimated_bytes > LARGE_RESULT_THRESHOLD {\n        tracker.check_large_result(estimated_bytes)?;\n    }\n    Ok(())\n}\n\n/// Converts an estimated bit count to bytes, saturating to `usize::MAX` on overflow.\n///\n/// Overflow means the result is astronomically large, so saturating ensures\n/// the resource limit check always triggers rather than being silently skipped.\nfn estimate_bits_to_bytes(bits: u64) -> usize {\n    usize::try_from(bits.saturating_add(7) / 8).unwrap_or(usize::MAX)\n}\n\n/// Error returned when a resource limit is exceeded during execution.\n///\n/// This allows the sandbox to enforce strict limits on allocation count,\n/// execution time, and memory usage.\n#[derive(Debug, Clone)]\npub enum ResourceError {\n    /// Maximum number of allocations exceeded.\n    Allocation { limit: usize, count: usize },\n    /// Maximum execution time exceeded.\n    Time { limit: Duration, elapsed: Duration },\n    /// Maximum memory usage exceeded.\n    Memory { limit: usize, used: usize },\n    /// Maximum recursion depth exceeded.\n    Recursion { limit: usize, depth: usize },\n    /// Any other error, e.g. when propagating a python exception\n    Exception(MontyException),\n}\n\nimpl fmt::Display for ResourceError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Allocation { limit, count } => {\n                write!(f, \"allocation limit exceeded: {count} > {limit}\")\n            }\n            Self::Time { limit, elapsed } => {\n                write!(f, \"time limit exceeded: {elapsed:?} > {limit:?}\")\n            }\n            Self::Memory { limit, used } => {\n                write!(f, \"memory limit exceeded: {used} bytes > {limit} bytes\")\n            }\n            Self::Recursion { .. } => {\n                write!(f, \"maximum recursion depth exceeded\")\n            }\n            Self::Exception(exc) => {\n                write!(f, \"{exc}\")\n            }\n        }\n    }\n}\n\nimpl std::error::Error for ResourceError {}\n\nimpl ResourceError {\n    /// Converts this resource error to a Python exception with optional stack frame.\n    ///\n    /// Maps resource error types to Python exception types:\n    /// - `Allocation` → `MemoryError`\n    /// - `Memory` → `MemoryError`\n    /// - `Time` → `TimeoutError`\n    /// - `Recursion` → `RecursionError`\n    #[must_use]\n    pub(crate) fn into_exception(self, frame: Option<RawStackFrame>) -> ExceptionRaise {\n        let (exc_type, msg) = match self {\n            Self::Allocation { limit, count } => (\n                ExcType::MemoryError,\n                Some(format!(\"allocation limit exceeded: {count} > {limit}\")),\n            ),\n            Self::Memory { limit, used } => (\n                ExcType::MemoryError,\n                Some(format!(\"memory limit exceeded: {used} bytes > {limit} bytes\")),\n            ),\n            Self::Time { limit, elapsed } => (\n                ExcType::TimeoutError,\n                Some(format!(\"time limit exceeded: {elapsed:?} > {limit:?}\")),\n            ),\n            Self::Recursion { .. } => (\n                ExcType::RecursionError,\n                Some(\"maximum recursion depth exceeded\".to_string()),\n            ),\n            Self::Exception(exc) => (exc.exc_type(), exc.into_message()),\n        };\n        let exc = SimpleException::new(exc_type, msg);\n        match frame {\n            Some(f) => exc.with_frame(f),\n            None => exc.into(),\n        }\n    }\n}\n\nimpl From<ResourceError> for RunError {\n    fn from(err: ResourceError) -> Self {\n        // RecursionError is catchable in CPython, so it must be catchable here too.\n        // Other resource errors (memory, time, allocation) remain uncatchable to prevent\n        // untrusted code from suppressing resource limit violations.\n        if matches!(err, ResourceError::Recursion { .. }) {\n            Self::Exc(err.into_exception(None))\n        } else {\n            Self::UncatchableExc(err.into_exception(None))\n        }\n    }\n}\n\n/// Trait for tracking resource usage and scheduling garbage collection.\n///\n/// Implementations can enforce limits on allocations, time, and memory,\n/// as well as schedule periodic garbage collection.\n///\n/// All implementations should eventually trigger garbage collection to handle\n/// reference cycles. The `should_gc` method controls *frequency*, not whether\n/// GC runs at all.\npub trait ResourceTracker: fmt::Debug {\n    /// Called before each heap allocation.\n    ///\n    /// Returns `Ok(())` if the allocation should proceed, or `Err(ResourceError)`\n    /// if a limit would be exceeded.\n    ///\n    /// # Arguments\n    /// * `size` - Approximate size in bytes of the allocation\n    fn on_allocate(&mut self, get_size: impl FnOnce() -> usize) -> Result<(), ResourceError>;\n\n    /// Called when memory is freed (during dec_ref or garbage collection).\n    ///\n    /// # Arguments\n    /// * `size` - Size in bytes of the freed allocation\n    fn on_free(&mut self, get_size: impl FnOnce() -> usize);\n\n    /// Called periodically (at statement boundaries) to check time limits.\n    ///\n    /// Returns `Ok(())` if within time limit, or `Err(ResourceError::Time)`\n    /// if the limit is exceeded.\n    ///\n    /// Takes `&self` rather than `&mut self` because checking elapsed time is a\n    /// read-only operation. This allows time checks in contexts that only have\n    /// an immutable heap reference, such as `py_repr_fmt`.\n    fn check_time(&self) -> Result<(), ResourceError>;\n\n    /// Called before pushing a new call frame to check recursion depth.\n    ///\n    /// Returns `Ok(())` if within recursion limit, or `Err(ResourceError::Recursion)`\n    /// if the limit would be exceeded.\n    ///\n    /// # Arguments\n    /// * `current_depth` - Current call stack depth (before the new frame is pushed)\n    fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError>;\n\n    /// Called before operations that may produce large results (>100KB).\n    ///\n    /// This allows pre-emptive rejection of operations like `2 ** 10_000_000`\n    /// before the memory is actually allocated. The check only happens for\n    /// estimated result sizes above `LARGE_RESULT_THRESHOLD` to avoid overhead\n    /// on small operations.\n    ///\n    /// # Arguments\n    /// * `estimated_bytes` - Approximate size of the result in bytes\n    ///\n    /// Returns `Ok(())` to allow the operation, or `Err(ResourceError)` to reject.\n    fn check_large_result(&self, estimated_bytes: usize) -> Result<(), ResourceError>;\n}\n\n/// A resource tracker that imposes no limits except default recursion limit.\n///\n/// Recursion limit is set to the cpython default of 1000.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct NoLimitTracker;\n\nimpl ResourceTracker for NoLimitTracker {\n    #[inline]\n    fn on_allocate(&mut self, _: impl FnOnce() -> usize) -> Result<(), ResourceError> {\n        Ok(())\n    }\n\n    #[inline]\n    fn on_free(&mut self, _: impl FnOnce() -> usize) {}\n\n    #[inline]\n    fn check_time(&self) -> Result<(), ResourceError> {\n        Ok(())\n    }\n\n    /// Set the recursion limit to 1000.\n    ///\n    /// The high limit here may cause stack overflow errors in debug mode, but do not those errors should\n    /// not occur with release builds.\n    #[inline]\n    fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError> {\n        const DEFAULT_RECURSION_LIMIT: usize = 1000;\n        if current_depth >= DEFAULT_RECURSION_LIMIT {\n            Err(ResourceError::Recursion {\n                limit: DEFAULT_RECURSION_LIMIT,\n                depth: current_depth + 1,\n            })\n        } else {\n            Ok(())\n        }\n    }\n\n    #[inline]\n    fn check_large_result(&self, _estimated_bytes: usize) -> Result<(), ResourceError> {\n        // No limit - always allow operations regardless of result size\n        Ok(())\n    }\n}\n\n/// Configuration for resource limits.\n///\n/// All limits are optional - set to `None` to disable a specific limit.\n/// Use `ResourceLimits::default()` for no limits, or build custom limits\n/// with the builder pattern.\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct ResourceLimits {\n    /// Maximum number of heap allocations allowed.\n    pub max_allocations: Option<usize>,\n    /// Maximum execution time.\n    pub max_duration: Option<Duration>,\n    /// Maximum heap memory in bytes (approximate).\n    pub max_memory: Option<usize>,\n    /// Run garbage collection every N allocations.\n    pub gc_interval: Option<usize>,\n    /// Maximum recursion depth (function call stack depth).\n    pub max_recursion_depth: Option<usize>,\n}\n\n/// Recommended maximum recursion depth if not otherwise specified.\npub const DEFAULT_MAX_RECURSION_DEPTH: usize = 1000;\n\nimpl ResourceLimits {\n    /// Creates a new ResourceLimits with all limits disabled, except max recursion which is set to 1000.\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            max_recursion_depth: Some(1000),\n            ..Default::default()\n        }\n    }\n\n    /// Sets the maximum number of allocations.\n    #[must_use]\n    pub fn max_allocations(mut self, limit: usize) -> Self {\n        self.max_allocations = Some(limit);\n        self\n    }\n\n    /// Sets the maximum execution duration.\n    #[must_use]\n    pub fn max_duration(mut self, limit: Duration) -> Self {\n        self.max_duration = Some(limit);\n        self\n    }\n\n    /// Sets the maximum memory usage in bytes.\n    #[must_use]\n    pub fn max_memory(mut self, limit: usize) -> Self {\n        self.max_memory = Some(limit);\n        self\n    }\n\n    /// Sets the garbage collection interval (run GC every N allocations).\n    #[must_use]\n    pub fn gc_interval(mut self, interval: usize) -> Self {\n        self.gc_interval = Some(interval);\n        self\n    }\n\n    /// Sets the maximum recursion depth (function call stack depth).\n    #[must_use]\n    pub fn max_recursion_depth(mut self, limit: Option<usize>) -> Self {\n        self.max_recursion_depth = limit;\n        self\n    }\n}\n\n/// How often to actually check `Instant::elapsed()` in `check_time`.\n///\n/// Calling `Instant::elapsed()` on every `check_time` invocation adds measurable\n/// overhead in tight loops (the VM calls `check_time` on every instruction).\n/// By only checking every N calls, we reduce this overhead while still catching\n/// timeouts promptly.\nconst TIME_CHECK_INTERVAL: u16 = 10;\n\n/// A resource tracker that enforces configurable limits.\n///\n/// Tracks allocation count, memory usage, and execution time, returning\n/// errors when limits are exceeded. Also schedules garbage collection\n/// at configurable intervals.\n///\n/// When serialized/deserialized, the `start_time` is reset to `Instant::now()`.\n/// This means time limits restart from zero after deserialization.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub struct LimitedTracker {\n    limits: ResourceLimits,\n    /// When execution started (for time limit checking).\n    /// Reset to `Instant::now()` on deserialization.\n    #[serde(skip, default = \"Instant::now\")]\n    start_time: Instant,\n    /// Total number of allocations made.\n    allocation_count: usize,\n    /// Current approximate memory usage in bytes.\n    current_memory: usize,\n    /// Counter for rate-limiting `Instant::elapsed()` calls in `check_time`.\n    ///\n    /// Uses `AtomicU16` for interior mutability since `check_time` takes `&self`\n    /// and `LimitedTracker` must be `Sync` (it ends up inside PyO3 pyclass types).\n    check_counter: AtomicU16,\n}\n\nimpl LimitedTracker {\n    /// Creates a new LimitedTracker with the given limits.\n    ///\n    /// The start time is recorded when the tracker is created, so create\n    /// it immediately before starting execution.\n    #[must_use]\n    pub fn new(limits: ResourceLimits) -> Self {\n        Self {\n            limits,\n            start_time: Instant::now(),\n            allocation_count: 0,\n            current_memory: 0,\n            check_counter: AtomicU16::new(0),\n        }\n    }\n\n    /// Returns the current allocation count.\n    #[must_use]\n    pub fn allocation_count(&self) -> usize {\n        self.allocation_count\n    }\n\n    /// Returns the current approximate memory usage.\n    #[must_use]\n    pub fn current_memory(&self) -> usize {\n        self.current_memory\n    }\n\n    /// Returns the elapsed time since tracker creation.\n    #[must_use]\n    pub fn elapsed(&self) -> Duration {\n        self.start_time.elapsed()\n    }\n\n    /// Sets the maximum execution duration and resets the start time to now.\n    ///\n    /// This is useful when resuming execution after an external function call\n    /// where you want to enforce a different (typically shorter) time limit\n    /// for the resumed phase without counting the time spent in the host.\n    pub fn set_max_duration(&mut self, duration: Duration) {\n        self.limits.max_duration = Some(duration);\n        self.start_time = Instant::now();\n    }\n}\n\nimpl ResourceTracker for LimitedTracker {\n    fn on_allocate(&mut self, get_size: impl FnOnce() -> usize) -> Result<(), ResourceError> {\n        // Check allocation count limit\n        if let Some(max) = self.limits.max_allocations\n            && self.allocation_count >= max\n        {\n            return Err(ResourceError::Allocation {\n                limit: max,\n                count: self.allocation_count + 1,\n            });\n        }\n\n        let size = get_size();\n        // Check memory limit\n        if let Some(max) = self.limits.max_memory {\n            let new_memory = self.current_memory + size;\n            if new_memory > max {\n                return Err(ResourceError::Memory {\n                    limit: max,\n                    used: new_memory,\n                });\n            }\n        }\n\n        // Update tracking state\n        self.allocation_count += 1;\n        self.current_memory += size;\n\n        Ok(())\n    }\n\n    fn on_free(&mut self, get_size: impl FnOnce() -> usize) {\n        self.current_memory = self.current_memory.saturating_sub(get_size());\n    }\n\n    fn check_time(&self) -> Result<(), ResourceError> {\n        if let Some(max) = self.limits.max_duration {\n            let count = self.check_counter.fetch_add(1, Ordering::Relaxed).wrapping_add(1);\n            if count.is_multiple_of(TIME_CHECK_INTERVAL) {\n                // Only call Instant::elapsed() every TIME_CHECK_INTERVAL calls\n                let elapsed = self.start_time.elapsed();\n                if elapsed > max {\n                    // Reset counter so the very next check_time call also triggers\n                    // an elapsed check. This is important because some callers\n                    // (e.g. repr_sequence_fmt) catch the error and return normally,\n                    // and we need the VM loop's next check_time to re-detect timeout.\n                    self.check_counter\n                        .store(TIME_CHECK_INTERVAL.wrapping_sub(1), Ordering::Relaxed);\n                    return Err(ResourceError::Time { limit: max, elapsed });\n                }\n            }\n        }\n        Ok(())\n    }\n\n    fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError> {\n        if let Some(max) = self.limits.max_recursion_depth {\n            // current_depth is before push, so new depth would be current_depth + 1\n            if current_depth >= max {\n                return Err(ResourceError::Recursion {\n                    limit: max,\n                    depth: current_depth + 1,\n                });\n            }\n        }\n        Ok(())\n    }\n\n    fn check_large_result(&self, estimated_bytes: usize) -> Result<(), ResourceError> {\n        // Check if this would exceed memory limit\n        if let Some(max) = self.limits.max_memory {\n            let new_memory = self.current_memory.saturating_add(estimated_bytes);\n            if new_memory > max {\n                return Err(ResourceError::Memory {\n                    limit: max,\n                    used: new_memory,\n                });\n            }\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/run.rs",
    "content": "//! Public interface for running Monty code.\nuse std::sync::atomic::{AtomicUsize, Ordering};\n\nuse crate::{\n    ExcType, MontyException,\n    bytecode::{Code, Compiler, FrameExit, VM},\n    exception_private::RunResult,\n    heap::{DropWithHeap, Heap},\n    intern::Interns,\n    io::PrintWriter,\n    object::MontyObject,\n    parse::parse,\n    prepare::prepare,\n    resource::{NoLimitTracker, ResourceTracker},\n    run_progress::{RunProgress, build_run_progress, check_snapshot_from_converted, convert_frame_exit},\n    value::Value,\n};\n\n/// Primary interface for running Monty code.\n///\n/// `MontyRun` supports two execution modes:\n/// - **Simple execution**: Use `run()` or `run_no_limits()` to run code to completion\n/// - **Iterative execution**: Use `start()` to start execution which will pause at external function calls and\n///   can be resumed later\n///\n/// # Example\n/// ```\n/// use monty::{MontyRun, MontyObject};\n///\n/// let runner = MontyRun::new(\"x + 1\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n/// let result = runner.run_no_limits(vec![MontyObject::Int(41)]).unwrap();\n/// assert_eq!(result, MontyObject::Int(42));\n/// ```\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct MontyRun {\n    /// The underlying executor containing parsed AST and interns.\n    executor: Executor,\n}\n\nimpl MontyRun {\n    /// Creates a new run snapshot by parsing the given code.\n    ///\n    /// This only parses and prepares the code - no heap or namespaces are created yet.\n    /// Call `run_snapshot()` with inputs to start execution.\n    ///\n    /// # Arguments\n    /// * `code` - The Python code to execute\n    /// * `script_name` - The script name for error messages\n    /// * `input_names` - Names of input variables\n    ///\n    /// # Errors\n    /// Returns `MontyException` if the code cannot be parsed.\n    pub fn new(code: String, script_name: &str, input_names: Vec<String>) -> Result<Self, MontyException> {\n        Executor::new(code, script_name, input_names).map(|executor| Self { executor })\n    }\n\n    /// Returns the code that was parsed to create this snapshot.\n    #[must_use]\n    pub fn code(&self) -> &str {\n        &self.executor.code\n    }\n\n    /// Executes the code and returns both the result and reference count data, used for testing only.\n    #[cfg(feature = \"ref-count-return\")]\n    pub fn run_ref_counts(&self, inputs: Vec<MontyObject>) -> Result<RefCountOutput, MontyException> {\n        self.executor.run_ref_counts(inputs)\n    }\n\n    /// Executes the code to completion assuming not external functions or snapshotting.\n    ///\n    /// This is marginally faster than running with snapshotting enabled since we don't need\n    /// to track the position in code, but does not allow calling of external functions.\n    ///\n    /// # Arguments\n    /// * `inputs` - Values to fill the first N slots of the namespace\n    /// * `resource_tracker` - Custom resource tracker implementation\n    /// * `print` - print output writer\n    pub fn run(\n        &self,\n        inputs: Vec<MontyObject>,\n        resource_tracker: impl ResourceTracker,\n        print: PrintWriter<'_>,\n    ) -> Result<MontyObject, MontyException> {\n        self.executor.run(inputs, resource_tracker, print)\n    }\n\n    /// Executes the code to completion with no resource limits, printing to stdout/stderr.\n    pub fn run_no_limits(&self, inputs: Vec<MontyObject>) -> Result<MontyObject, MontyException> {\n        self.run(inputs, NoLimitTracker, PrintWriter::Stdout)\n    }\n\n    /// Serializes the runner to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `load()`.\n    /// This allows caching parsed code to avoid re-parsing on subsequent runs.\n    ///\n    /// # Errors\n    /// Returns an error if serialization fails.\n    pub fn dump(&self) -> Result<Vec<u8>, postcard::Error> {\n        postcard::to_allocvec(self)\n    }\n\n    /// Deserializes a runner from binary format.\n    ///\n    /// # Arguments\n    /// * `bytes` - The serialized runner data from `dump()`\n    ///\n    /// # Errors\n    /// Returns an error if deserialization fails.\n    pub fn load(bytes: &[u8]) -> Result<Self, postcard::Error> {\n        postcard::from_bytes(bytes)\n    }\n\n    /// Starts execution with the given inputs and resource tracker, consuming self.\n    ///\n    /// Creates the heap and namespaces, then begins execution.\n    ///\n    /// For iterative execution, `start()` consumes self and returns a `RunProgress`:\n    /// - `RunProgress::FunctionCall(call)` - external function call, call `call.resume(return_value)` to resume\n    /// - `RunProgress::Complete(value)` - execution finished\n    ///\n    /// This enables snapshotting execution state and returning control to the host\n    /// application during long-running computations.\n    ///\n    /// # Arguments\n    /// * `inputs` - Initial input values (must match length of `input_names` from `new()`)\n    /// * `resource_tracker` - Resource tracker for the execution\n    /// * `print` - Writer for print output\n    ///\n    /// # Errors\n    /// Returns `MontyException` if:\n    /// - The number of inputs doesn't match the expected count\n    /// - An input value is invalid (e.g., `MontyObject::Repr`)\n    /// - A runtime error occurs during execution\n    ///\n    /// # Panics\n    /// This method should not panic under normal operation. Internal assertions\n    /// may panic if the VM reaches an inconsistent state (indicating a bug).\n    pub fn start<T: ResourceTracker>(\n        self,\n        inputs: Vec<MontyObject>,\n        resource_tracker: T,\n        print: PrintWriter<'_>,\n    ) -> Result<RunProgress<T>, MontyException> {\n        let executor = self.executor;\n\n        // Create heap and VM with empty globals, then populate inputs with VM alive\n        let mut heap = Heap::new(executor.namespace_size, resource_tracker);\n        let globals = executor.empty_globals();\n        let mut vm = VM::new(globals, &mut heap, &executor.interns, print);\n        executor.populate_inputs(inputs, &mut vm)?;\n\n        // Start execution\n        let vm_result = vm.run_module(&executor.module_code);\n\n        // Three-phase conversion: convert while VM alive, then snapshot, then build progress\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        let vm_state = check_snapshot_from_converted(&converted, vm);\n        build_run_progress(converted, vm_state, executor, heap)\n    }\n}\n\n/// Lower level interface to parse code and run it to completion.\n///\n/// This is an internal type used by [`MontyRun`]. It stores the compiled bytecode and source code\n/// for error reporting. Also used by `run_progress` and `repl` modules.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct Executor {\n    /// Number of slots needed in the global namespace.\n    pub(crate) namespace_size: usize,\n    /// Maps variable names to their indices in the namespace. Used for ref-count testing.\n    #[cfg(feature = \"ref-count-return\")]\n    name_map: ahash::AHashMap<String, crate::namespace::NamespaceId>,\n    /// Compiled bytecode for the module.\n    pub(crate) module_code: Code,\n    /// Interned strings used for looking up names and filenames during execution.\n    pub(crate) interns: Interns,\n    /// Source code for error reporting (extracting preview lines for tracebacks).\n    pub(crate) code: String,\n    /// Estimated heap capacity for pre-allocation on subsequent runs.\n    /// Uses AtomicUsize for thread-safety (required by PyO3's Sync bound).\n    heap_capacity: AtomicUsize,\n}\n\nimpl Clone for Executor {\n    fn clone(&self) -> Self {\n        Self {\n            namespace_size: self.namespace_size,\n            #[cfg(feature = \"ref-count-return\")]\n            name_map: self.name_map.clone(),\n            module_code: self.module_code.clone(),\n            interns: self.interns.clone(),\n            code: self.code.clone(),\n            heap_capacity: AtomicUsize::new(self.heap_capacity.load(Ordering::Relaxed)),\n        }\n    }\n}\n\nimpl Executor {\n    /// Creates a new executor with the given code, filename, and input names.\n    pub(crate) fn new(code: String, script_name: &str, input_names: Vec<String>) -> Result<Self, MontyException> {\n        let parse_result = parse(&code, script_name).map_err(|e| e.into_python_exc(script_name, &code))?;\n        let prepared = prepare(parse_result, input_names).map_err(|e| e.into_python_exc(script_name, &code))?;\n\n        // Create interns with empty functions (functions will be set after compilation)\n        let mut interns = Interns::new(prepared.interner, Vec::new());\n\n        // Compile the module to bytecode, which also compiles all nested functions\n        let namespace_size_u16 = u16::try_from(prepared.namespace_size).expect(\"module namespace size exceeds u16\");\n        let compile_result = Compiler::compile_module(&prepared.nodes, &interns, namespace_size_u16)\n            .map_err(|e| e.into_python_exc(script_name, &code))?;\n\n        // Set the compiled functions in the interns\n        interns.set_functions(compile_result.functions);\n\n        Ok(Self {\n            namespace_size: prepared.namespace_size,\n            #[cfg(feature = \"ref-count-return\")]\n            name_map: prepared.name_map,\n            module_code: compile_result.code,\n            interns,\n            code,\n            heap_capacity: AtomicUsize::new(prepared.namespace_size),\n        })\n    }\n\n    /// Executes the code with a custom resource tracker.\n    ///\n    /// This provides full control over resource tracking and garbage collection\n    /// scheduling. The tracker is called on each allocation and periodically\n    /// during execution to check time limits and trigger GC.\n    ///\n    /// # Arguments\n    /// * `inputs` - Values to fill the first N slots of the namespace\n    /// * `resource_tracker` - Custom resource tracker implementation\n    /// * `print` - Print output writer\n    fn run(\n        &self,\n        inputs: Vec<MontyObject>,\n        resource_tracker: impl ResourceTracker,\n        print: PrintWriter<'_>,\n    ) -> Result<MontyObject, MontyException> {\n        let heap_capacity = self.heap_capacity.load(Ordering::Relaxed);\n        let mut heap = Heap::new(heap_capacity, resource_tracker);\n        let globals = self.empty_globals();\n\n        // Create VM first, then populate inputs with VM alive\n        let mut vm = VM::new(globals, &mut heap, &self.interns, print);\n        self.populate_inputs(inputs, &mut vm)?;\n        let mut frame_exit_result = vm.run_module(&self.module_code);\n\n        // Handle NameLookup and ExternalCall exits by raising NameError through the VM\n        // so that traceback information is properly captured. In the non-iterative path,\n        // there's no host to resolve names or external functions, so these become NameErrors.\n        loop {\n            match frame_exit_result {\n                Ok(FrameExit::NameLookup { name_id, .. }) => {\n                    let name = self.interns.get_str(name_id);\n                    let err = ExcType::name_error(name);\n                    frame_exit_result = vm.resume_with_exception(err.into());\n                }\n                Ok(FrameExit::ExternalCall {\n                    function_name,\n                    args,\n                    name_load_ip,\n                    ..\n                }) => {\n                    // In standard execution, an ExtFunction from LoadGlobalCallable/\n                    // LoadLocalCallable means the name was undefined — raise NameError.\n                    // Restore the frame IP to the load instruction so the traceback\n                    // points to the name reference, not the call expression.\n                    if let Some(load_ip) = name_load_ip {\n                        vm.set_instruction_ip(load_ip);\n                    }\n                    let name = function_name.as_str(&self.interns);\n                    args.drop_with_heap(vm.heap);\n                    let err = ExcType::name_error(name);\n                    frame_exit_result = vm.resume_with_exception(err.into());\n                }\n                _ => break,\n            }\n        }\n\n        // Convert output while VM is still alive\n        let result = frame_exit_to_object(frame_exit_result, &mut vm);\n\n        // Clean up VM state before it goes out of scope\n        vm.cleanup();\n\n        if heap.size() > heap_capacity {\n            self.heap_capacity.store(heap.size(), Ordering::Relaxed);\n        }\n\n        result.map_err(|e| e.into_python_exception(&self.interns, &self.code))\n    }\n\n    /// Executes the code and returns both the result and reference count data, used for testing only.\n    ///\n    /// This is used for testing reference counting behavior. Returns:\n    /// - The execution result (`Exit`)\n    /// - Reference count data as a tuple of:\n    ///   - A map from variable names to their reference counts (only for heap-allocated values)\n    ///   - The number of unique heap value IDs referenced by variables\n    ///   - The total number of live heap values\n    ///\n    /// For strict matching validation, compare unique_refs_count with heap_entry_count.\n    /// If they're equal, all heap values are accounted for by named variables.\n    ///\n    /// Only available when the `ref-count-return` feature is enabled.\n    #[cfg(feature = \"ref-count-return\")]\n    fn run_ref_counts(&self, inputs: Vec<MontyObject>) -> Result<RefCountOutput, MontyException> {\n        use std::collections::HashSet;\n\n        let mut heap = Heap::new(self.namespace_size, NoLimitTracker);\n        let globals = self.empty_globals();\n\n        // Create VM, populate inputs, and run\n        let mut vm = VM::new(globals, &mut heap, &self.interns, PrintWriter::Stdout);\n        self.populate_inputs(inputs, &mut vm)?;\n        let frame_exit_result = vm.run_module(&self.module_code);\n\n        // Take globals out of the VM so we can inspect them, but keep VM alive\n        // for heap access and later conversion.\n        let globals = vm.take_globals();\n\n        // Read refcounts BEFORE converting the return value, because\n        // `frame_exit_to_object` drops the return value (decrementing its refcount).\n        let mut counts = ahash::AHashMap::new();\n        let mut unique_ids = HashSet::new();\n\n        for (name, &namespace_id) in &self.name_map {\n            let idx = namespace_id.index();\n            if idx < globals.len()\n                && let Value::Ref(id) = &globals[idx]\n            {\n                counts.insert(name.clone(), vm.heap.get_refcount(*id));\n                unique_ids.insert(*id);\n            }\n        }\n        let unique_refs = unique_ids.len();\n        let heap_count = vm.heap.entry_count();\n\n        // Convert return value while VM is still alive (needs access to interns)\n        let py_object = frame_exit_to_object(frame_exit_result, &mut vm)\n            .map_err(|e| e.into_python_exception(&self.interns, &self.code))?;\n\n        vm.cleanup();\n\n        // Drop globals with proper ref counting\n        for value in globals {\n            value.drop_with_heap(&mut heap);\n        }\n\n        let allocations_since_gc = heap.get_allocations_since_gc();\n\n        Ok(RefCountOutput {\n            py_object,\n            counts,\n            unique_refs,\n            heap_count,\n            allocations_since_gc,\n        })\n    }\n\n    /// Creates an empty globals vector with all slots set to `Undefined`.\n    ///\n    /// Used to initialize global storage before input population. The VM is created\n    /// with these empty globals, then [`populate_inputs`](Self::populate_inputs) fills\n    /// the input slots while the VM is alive.\n    pub(crate) fn empty_globals(&self) -> Vec<Value> {\n        (0..self.namespace_size).map(|_| Value::Undefined).collect()\n    }\n\n    /// Converts `MontyObject` inputs to `Value`s and writes them into the VM's globals.\n    ///\n    /// This runs with the VM alive so that `to_value` has access to the full VM context.\n    /// On error partway through, the VM's `cleanup()` (via drop) will drain globals and\n    /// properly decrement refcounts for any already-converted values.\n    pub(crate) fn populate_inputs(\n        &self,\n        inputs: Vec<MontyObject>,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<(), MontyException> {\n        if inputs.len() > self.namespace_size {\n            return Err(MontyException::runtime_error(\"too many inputs for namespace\"));\n        }\n        for (i, input) in inputs.into_iter().enumerate() {\n            let value = input\n                .to_value(vm)\n                .map_err(|e| MontyException::runtime_error(format!(\"invalid input type: {e}\")))?;\n            vm.globals[i] = value;\n        }\n        Ok(())\n    }\n}\n\n/// Converts module/frame exit results into plain `MontyObject` outputs.\n///\n/// Used by non-iterative execution paths where suspendable outcomes (external calls,\n/// name lookups) are not supported and should produce errors.\nfn frame_exit_to_object(\n    frame_exit_result: RunResult<FrameExit>,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<MontyObject> {\n    match frame_exit_result? {\n        FrameExit::Return(return_value) => Ok(MontyObject::new(return_value, vm)),\n        FrameExit::ExternalCall {\n            function_name, args, ..\n        } => {\n            args.drop_with_heap(vm.heap);\n            let function_name = function_name.as_str(vm.interns);\n            Err(ExcType::not_implemented(format!(\n                \"External function '{function_name}' not implemented with standard execution\"\n            ))\n            .into())\n        }\n        FrameExit::OsCall { function, args, .. } => {\n            args.drop_with_heap(vm.heap);\n            Err(ExcType::not_implemented(format!(\n                \"OS function '{function}' not implemented with standard execution\"\n            ))\n            .into())\n        }\n        FrameExit::MethodCall { method_name, args, .. } => {\n            args.drop_with_heap(vm.heap);\n            let name = method_name.as_str(vm.interns);\n            Err(\n                ExcType::not_implemented(format!(\"Method call '{name}' not implemented with standard execution\"))\n                    .into(),\n            )\n        }\n        FrameExit::ResolveFutures(_) => {\n            Err(ExcType::not_implemented(\"async futures not supported by standard execution.\").into())\n        }\n        FrameExit::NameLookup { name_id, .. } => {\n            let name = vm.interns.get_str(name_id);\n            Err(ExcType::name_error(name).into())\n        }\n    }\n}\n\n/// Output from `run_ref_counts` containing reference count and heap information.\n///\n/// Used for testing GC behavior and reference counting correctness.\n#[cfg(feature = \"ref-count-return\")]\n#[derive(Debug)]\npub struct RefCountOutput {\n    pub py_object: MontyObject,\n    pub counts: ahash::AHashMap<String, usize>,\n    pub unique_refs: usize,\n    pub heap_count: usize,\n    /// Number of GC-tracked allocations since the last garbage collection.\n    ///\n    /// If GC ran during execution, this will be lower than the total number of\n    /// allocations. Compare this against expected allocation count to verify GC ran.\n    pub allocations_since_gc: u32,\n}\n"
  },
  {
    "path": "crates/monty/src/run_progress.rs",
    "content": "//! This module defines the public types returned by [`MontyRun::start()`](crate::MontyRun::start)\n//! and their resume methods. Each variant of [`RunProgress`] wraps a dedicated struct\n//! (`FunctionCall`, `OsCall`, `NameLookup`, `ResolveFutures`) that carries only the\n//! fields and resume methods relevant to that suspension point.\n//!\n//! The internal [`Snapshot`] type is `pub(crate)` — callers interact exclusively with\n//! the per-variant structs.\n\nuse std::mem;\n\nuse crate::{\n    ExcType, MontyException,\n    asyncio::CallId,\n    bytecode::{FrameExit, VM, VMSnapshot},\n    exception_private::{RunError, RunResult},\n    heap::Heap,\n    io::PrintWriter,\n    object::MontyObject,\n    os::OsFunction,\n    resource::ResourceTracker,\n    run::Executor,\n    value::Value,\n};\n\n// ---------------------------------------------------------------------------\n// RunProgress enum\n// ---------------------------------------------------------------------------\n\n/// Result of a single step of iterative execution.\n///\n/// Each variant wraps a dedicated struct that owns the execution state and\n/// exposes only the resume methods relevant to that suspension reason.\n///\n/// # Type Parameters\n/// * `T` — Resource tracker implementation (e.g. `NoLimitTracker` or `LimitedTracker`).\n///\n/// Serialization requires `T: Serialize + Deserialize`.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub enum RunProgress<T: ResourceTracker> {\n    /// Execution paused at an external function call or dataclass method call.\n    FunctionCall(FunctionCall<T>),\n    /// Execution paused for an OS-level operation (filesystem, network, etc.).\n    OsCall(OsCall<T>),\n    /// All async tasks are blocked waiting for external futures to resolve.\n    ResolveFutures(ResolveFutures<T>),\n    /// Execution paused for an unresolved name lookup.\n    NameLookup(NameLookup<T>),\n    /// Execution completed with a final result.\n    Complete(MontyObject),\n}\n\nimpl<T: ResourceTracker> RunProgress<T> {\n    /// Consumes the progress and returns the `FunctionCall` struct if this is a function call.\n    #[must_use]\n    pub fn into_function_call(self) -> Option<FunctionCall<T>> {\n        match self {\n            Self::FunctionCall(call) => Some(call),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the `OsCall` struct if this is an OS call.\n    #[must_use]\n    pub fn into_os_call(self) -> Option<OsCall<T>> {\n        match self {\n            Self::OsCall(call) => Some(call),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the final value if execution completed.\n    #[must_use]\n    pub fn into_complete(self) -> Option<MontyObject> {\n        match self {\n            Self::Complete(value) => Some(value),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the `ResolveFutures` struct.\n    #[must_use]\n    pub fn into_resolve_futures(self) -> Option<ResolveFutures<T>> {\n        match self {\n            Self::ResolveFutures(state) => Some(state),\n            _ => None,\n        }\n    }\n\n    /// Consumes the progress and returns the `NameLookup` struct.\n    #[must_use]\n    pub fn into_name_lookup(self) -> Option<NameLookup<T>> {\n        match self {\n            Self::NameLookup(lookup) => Some(lookup),\n            _ => None,\n        }\n    }\n}\n\nimpl<T: ResourceTracker + serde::Serialize> RunProgress<T> {\n    /// Serializes the execution state to a binary format.\n    ///\n    /// # Errors\n    /// Returns an error if serialization fails.\n    pub fn dump(&self) -> Result<Vec<u8>, postcard::Error> {\n        postcard::to_allocvec(self)\n    }\n}\n\nimpl<T: ResourceTracker + serde::de::DeserializeOwned> RunProgress<T> {\n    /// Deserializes execution state from binary format.\n    ///\n    /// # Errors\n    /// Returns an error if deserialization fails.\n    pub fn load(bytes: &[u8]) -> Result<Self, postcard::Error> {\n        postcard::from_bytes(bytes)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// FunctionCall\n// ---------------------------------------------------------------------------\n\n/// Execution paused at an external function call or dataclass method call.\n///\n/// The host can choose how to handle this:\n/// - **Sync resolution**: Call `resume(return_value, print)` to push the result and continue.\n/// - **Async resolution**: Call `resume_pending(print)` to push an `ExternalFuture` and continue.\n///\n/// When using async resolution, the code continues and may `await` the future later.\n/// If the future isn't resolved when awaited, execution yields with `ResolveFutures`.\n///\n/// When `method_call` is true, this represents a dataclass method call where the first\n/// positional arg is the dataclass instance (`self`).\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct FunctionCall<T: ResourceTracker> {\n    /// The name of the function or method being called.\n    pub function_name: String,\n    /// The positional arguments passed to the function.\n    pub args: Vec<MontyObject>,\n    /// The keyword arguments passed to the function (key, value pairs).\n    pub kwargs: Vec<(MontyObject, MontyObject)>,\n    /// Unique identifier for this call (used for async correlation).\n    pub call_id: u32,\n    /// Whether this is a dataclass method call (first arg is `self`).\n    pub method_call: bool,\n    /// Internal execution snapshot.\n    snapshot: Snapshot<T>,\n}\n\nimpl<T: ResourceTracker> FunctionCall<T> {\n    /// Creates a new `FunctionCall` from its parts.\n    fn new(\n        function_name: String,\n        args: Vec<MontyObject>,\n        kwargs: Vec<(MontyObject, MontyObject)>,\n        call_id: u32,\n        method_call: bool,\n        snapshot: Snapshot<T>,\n    ) -> Self {\n        Self {\n            function_name,\n            args,\n            kwargs,\n            call_id,\n            method_call,\n            snapshot,\n        }\n    }\n\n    /// Returns a mutable reference to the resource tracker.\n    ///\n    /// This allows modifying resource limits between execution phases,\n    /// e.g. setting a time limit before resuming after an external function call.\n    pub fn tracker_mut(&mut self) -> &mut T {\n        self.snapshot.heap.tracker_mut()\n    }\n\n    /// Resumes execution with the return value or exception from the external function.\n    ///\n    /// Consumes self and returns the next execution progress.\n    ///\n    /// # Arguments\n    /// * `result` — The return value, exception, or pending future marker.\n    /// * `print` — Writer for `print()` output.\n    pub fn resume(\n        self,\n        result: impl Into<ExtFunctionResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<RunProgress<T>, MontyException> {\n        self.snapshot.run(result, print)\n    }\n\n    /// Resumes execution by pushing an `ExternalFuture` instead of a concrete value.\n    ///\n    /// This is the async resolution pattern: the host continues execution with a\n    /// pending future. The code can then `await` this future later. If the code\n    /// awaits the future before it's resolved, execution will yield with\n    /// `RunProgress::ResolveFutures`.\n    ///\n    /// Uses `self.call_id` internally — no need to pass it again.\n    ///\n    /// # Arguments\n    /// * `print` — Writer for print output.\n    pub fn resume_pending(self, print: PrintWriter<'_>) -> Result<RunProgress<T>, MontyException> {\n        self.snapshot.run(ExtFunctionResult::Future(self.call_id), print)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// OsCall\n// ---------------------------------------------------------------------------\n\n/// Execution paused for an OS-level operation.\n///\n/// The host should execute the OS operation (filesystem, network, etc.) and\n/// call `resume(return_value, print)` to provide the result and continue.\n///\n/// This enables sandboxed execution where the interpreter never directly performs I/O.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct OsCall<T: ResourceTracker> {\n    /// The OS function to execute.\n    pub function: OsFunction,\n    /// The positional arguments for the OS function.\n    pub args: Vec<MontyObject>,\n    /// The keyword arguments passed to the function (key, value pairs).\n    pub kwargs: Vec<(MontyObject, MontyObject)>,\n    /// Unique identifier for this call (used for async correlation).\n    pub call_id: u32,\n    /// Internal execution snapshot.\n    snapshot: Snapshot<T>,\n}\n\nimpl<T: ResourceTracker> OsCall<T> {\n    /// Creates a new `OsCall` from its parts.\n    fn new(\n        function: OsFunction,\n        args: Vec<MontyObject>,\n        kwargs: Vec<(MontyObject, MontyObject)>,\n        call_id: u32,\n        snapshot: Snapshot<T>,\n    ) -> Self {\n        Self {\n            function,\n            args,\n            kwargs,\n            call_id,\n            snapshot,\n        }\n    }\n\n    /// Resumes execution with the OS call result.\n    ///\n    /// # Arguments\n    /// * `result` — The return value or exception from the OS operation.\n    /// * `print` — Writer for `print()` output.\n    pub fn resume(\n        self,\n        result: impl Into<ExtFunctionResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<RunProgress<T>, MontyException> {\n        self.snapshot.run(result, print)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// NameLookup\n// ---------------------------------------------------------------------------\n\n/// Execution paused for an unresolved name lookup.\n///\n/// The host should check if the name corresponds to a known external function or\n/// value. Call `resume(result, print)` with `NameLookupResult::Value(obj)` to\n/// cache it in the namespace and continue, or `NameLookupResult::Undefined` to\n/// raise `NameError`.\n///\n/// The namespace slot and scope are managed internally — the host only needs to\n/// provide the name resolution result.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct NameLookup<T: ResourceTracker> {\n    /// The name being looked up.\n    pub name: String,\n    /// The namespace slot where the resolved value should be cached.\n    namespace_slot: u16,\n    /// Whether this is a global slot or a local/function slot.\n    is_global: bool,\n    /// Internal execution snapshot.\n    snapshot: Snapshot<T>,\n}\n\nimpl<T: ResourceTracker> NameLookup<T> {\n    /// Creates a new `NameLookup` from its parts.\n    fn new(name: String, namespace_slot: u16, is_global: bool, snapshot: Snapshot<T>) -> Self {\n        Self {\n            name,\n            namespace_slot,\n            is_global,\n            snapshot,\n        }\n    }\n\n    /// Resumes execution after name resolution.\n    ///\n    /// Caches the resolved value in the appropriate slot (globals or stack)\n    /// before restoring the VM, then either pushes the value or raises `NameError`.\n    ///\n    /// # Arguments\n    /// * `result` — The resolved value or `Undefined`.\n    /// * `print` — Writer for print output.\n    pub fn resume(\n        mut self,\n        result: impl Into<NameLookupResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<RunProgress<T>, MontyException> {\n        // Restore the VM first, then convert inside its lifetime\n        let mut vm = VM::restore(\n            self.snapshot.vm_state,\n            &self.snapshot.executor.module_code,\n            &mut self.snapshot.heap,\n            &self.snapshot.executor.interns,\n            print,\n        );\n\n        // Resolve the name lookup result with the VM alive\n        let vm_result = match result.into() {\n            NameLookupResult::Value(obj) => {\n                let value = obj.to_value(&mut vm).map_err(|e| {\n                    vm.cleanup();\n                    MontyException::runtime_error(format!(\"invalid name lookup result: {e}\"))\n                })?;\n\n                // Cache the resolved value in the appropriate slot\n                let slot = self.namespace_slot as usize;\n                if self.is_global {\n                    let cloned = value.clone_with_heap(vm.heap);\n                    let old = mem::replace(&mut vm.globals[slot], cloned);\n                    old.drop_with_heap(vm.heap);\n                } else {\n                    let stack_base = vm.current_stack_base();\n                    let cloned = value.clone_with_heap(vm.heap);\n                    let old = mem::replace(&mut vm.stack[stack_base + slot], cloned);\n                    old.drop_with_heap(vm.heap);\n                }\n\n                vm.push(value);\n                vm.run()\n            }\n            NameLookupResult::Undefined => {\n                let err = ExcType::name_error(&self.name);\n                vm.resume_with_exception(err.into())\n            }\n        };\n\n        // Three-phase: convert while VM alive, snapshot, build progress\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        let vm_state = check_snapshot_from_converted(&converted, vm);\n        build_run_progress(converted, vm_state, self.snapshot.executor, self.snapshot.heap)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// ResolveFutures\n// ---------------------------------------------------------------------------\n\n/// Execution state paused while waiting for external future results.\n///\n/// Supports incremental resolution — you can provide partial results and Monty\n/// will continue running until all tasks are blocked again.\n///\n/// Use `pending_call_ids()` to see which calls are pending, then call\n/// `resume(results, print)` with some or all of the results.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub struct ResolveFutures<T: ResourceTracker> {\n    /// The executor containing compiled code and interns.\n    executor: Executor,\n    /// The VM state containing stack, frames, globals, and exception state.\n    vm_state: VMSnapshot,\n    /// The heap containing all allocated objects.\n    heap: Heap<T>,\n    /// The pending call_ids that this snapshot is waiting on.\n    pending_call_ids: Vec<u32>,\n}\n\nimpl<T: ResourceTracker> ResolveFutures<T> {\n    /// Creates a new `ResolveFutures` from its parts.\n    fn new(executor: Executor, vm_state: VMSnapshot, heap: Heap<T>, pending_call_ids: Vec<u32>) -> Self {\n        Self {\n            executor,\n            vm_state,\n            heap,\n            pending_call_ids,\n        }\n    }\n\n    /// Returns unresolved call IDs for this suspended state.\n    #[must_use]\n    pub fn pending_call_ids(&self) -> &[u32] {\n        &self.pending_call_ids\n    }\n\n    /// Resumes execution with results for some or all pending futures.\n    ///\n    /// **Incremental resolution**: You don't need to provide all results at once.\n    /// If you provide a partial list, Monty will:\n    /// 1. Mark those futures as resolved\n    /// 2. Unblock any tasks waiting on those futures\n    /// 3. Continue running until all tasks are blocked again\n    /// 4. Return `ResolveFutures` with the remaining pending calls\n    ///\n    /// # Arguments\n    /// * `results` — List of `(call_id, result)` pairs. Can be a subset of pending calls.\n    /// * `print` — Writer for print output.\n    ///\n    /// # Errors\n    /// Returns `Err(MontyException)` if any `call_id` in `results` is not in the pending set.\n    pub fn resume(\n        self,\n        results: Vec<(u32, ExtFunctionResult)>,\n        print: PrintWriter<'_>,\n    ) -> Result<RunProgress<T>, MontyException> {\n        let Self {\n            executor,\n            vm_state,\n            mut heap,\n            pending_call_ids,\n        } = self;\n\n        // Validate that all provided call_ids are in the pending set before restoring VM.\n        let invalid_call_id = results\n            .iter()\n            .find(|(call_id, _)| !pending_call_ids.contains(call_id))\n            .map(|(call_id, _)| *call_id);\n\n        // Restore the VM from the snapshot (must happen before any error return to clean up properly).\n        let mut vm = VM::restore(vm_state, &executor.module_code, &mut heap, &executor.interns, print);\n\n        // Now check for invalid call_ids after VM is restored.\n        if let Some(call_id) = invalid_call_id {\n            vm.cleanup();\n            return Err(MontyException::runtime_error(format!(\n                \"unknown call_id {call_id}, expected one of: {pending_call_ids:?}\"\n            )));\n        }\n\n        for (call_id, ext_result) in results {\n            match ext_result {\n                ExtFunctionResult::Return(obj) => vm.resolve_future(call_id, obj).map_err(|e| {\n                    MontyException::runtime_error(format!(\"Invalid return type for call {call_id}: {e}\"))\n                })?,\n                ExtFunctionResult::Error(exc) => vm.fail_future(call_id, exc.into()),\n                ExtFunctionResult::Future(_) => {}\n                ExtFunctionResult::NotFound(function_name) => {\n                    vm.fail_future(call_id, ExtFunctionResult::not_found_exc(&function_name));\n                }\n            }\n        }\n\n        // Check if the current task has failed.\n        if let Some(error) = vm.take_failed_task_error() {\n            vm.cleanup();\n            return Err(error.into_python_exception(&executor.interns, &executor.code));\n        }\n\n        // Push resolved value for main task if it was blocked.\n        let main_task_ready = vm.prepare_current_task_after_resolve();\n\n        let loaded_task = match vm.load_ready_task_if_needed() {\n            Ok(loaded) => loaded,\n            Err(e) => {\n                vm.cleanup();\n                return Err(e.into_python_exception(&executor.interns, &executor.code));\n            }\n        };\n\n        // If no task is ready and there are still pending calls, return ResolveFutures.\n        if !main_task_ready && !loaded_task {\n            let pending_call_ids = vm.get_pending_call_ids();\n            if !pending_call_ids.is_empty() {\n                let vm_state = vm.snapshot();\n                let pending_call_ids: Vec<u32> = pending_call_ids.iter().map(|id| id.raw()).collect();\n                return Ok(RunProgress::ResolveFutures(Self {\n                    executor,\n                    vm_state,\n                    heap,\n                    pending_call_ids,\n                }));\n            }\n        }\n\n        let result = vm.run();\n\n        // Three-phase: convert while VM alive, snapshot, build progress\n        let converted = convert_frame_exit(result, &mut vm);\n        let vm_state = check_snapshot_from_converted(&converted, vm);\n        build_run_progress(converted, vm_state, executor, heap)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot (pub(crate))\n// ---------------------------------------------------------------------------\n\n/// Internal execution state that can be resumed after suspension.\n///\n/// This is a `pub(crate)` implementation detail wrapped by the per-variant\n/// structs (`FunctionCall`, `OsCall`, `NameLookup`). It is not exposed in the\n/// public API.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\n#[serde(bound(serialize = \"T: serde::Serialize\", deserialize = \"T: serde::de::DeserializeOwned\"))]\npub(crate) struct Snapshot<T: ResourceTracker> {\n    /// The executor containing compiled code and interns.\n    pub(crate) executor: Executor,\n    /// The VM state containing stack, frames, globals, and exception state.\n    pub(crate) vm_state: VMSnapshot,\n    /// The heap containing all allocated objects.\n    pub(crate) heap: Heap<T>,\n}\n\nimpl<T: ResourceTracker> Snapshot<T> {\n    /// Continues execution with the return value or exception from the external call.\n    pub(crate) fn run(\n        mut self,\n        result: impl Into<ExtFunctionResult>,\n        print: PrintWriter<'_>,\n    ) -> Result<RunProgress<T>, MontyException> {\n        let ext_result = result.into();\n\n        let mut vm = VM::restore(\n            self.vm_state,\n            &self.executor.module_code,\n            &mut self.heap,\n            &self.executor.interns,\n            print,\n        );\n\n        let vm_result = match ext_result {\n            ExtFunctionResult::Return(obj) => vm.resume(obj),\n            ExtFunctionResult::Error(exc) => vm.resume_with_exception(exc.into()),\n            ExtFunctionResult::Future(raw_call_id) => {\n                let call_id = CallId::new(raw_call_id);\n                vm.add_pending_call(call_id);\n                vm.push(Value::ExternalFuture(call_id));\n                vm.run()\n            }\n            ExtFunctionResult::NotFound(function_name) => {\n                vm.resume_with_exception(ExtFunctionResult::not_found_exc(&function_name))\n            }\n        };\n\n        // Three-phase: convert while VM alive, snapshot, build progress\n        let converted = convert_frame_exit(vm_result, &mut vm);\n        let vm_state = check_snapshot_from_converted(&converted, vm);\n        build_run_progress(converted, vm_state, self.executor, self.heap)\n    }\n}\n\n/// Result of a name lookup from the host.\n///\n/// When the VM encounters an unresolved name, the host provides one of these:\n/// - `Value(obj)`: The name resolves to this value (cached in the namespace for future access).\n/// - `Undefined`: The name is truly undefined, causing `NameError`.\n#[derive(Debug)]\npub enum NameLookupResult {\n    /// The name resolves to this value.\n    Value(MontyObject),\n    /// The name is undefined — VM will raise `NameError`.\n    Undefined,\n}\n\nimpl From<MontyObject> for NameLookupResult {\n    fn from(value: MontyObject) -> Self {\n        Self::Value(value)\n    }\n}\n\n/// Return value or exception from an external function.\n#[derive(Debug)]\npub enum ExtFunctionResult {\n    /// Continues execution with the return value from the external function.\n    Return(MontyObject),\n    /// Continues execution with the exception raised by the external function.\n    Error(MontyException),\n    /// Pending future — the external function is a coroutine.\n    ///\n    /// The `u32` is the `call_id` from the `FunctionCall` that created this\n    /// snapshot. It is used to track the pending future so it can be resolved\n    /// later via `ResolveFutures::resume()`.\n    Future(u32),\n    /// The function was not found, should result in a `NameError` exception.\n    NotFound(String),\n}\n\nimpl ExtFunctionResult {\n    pub(crate) fn not_found_exc(function_name: &str) -> RunError {\n        let msg = format!(\"name '{function_name}' is not defined\");\n        MontyException::new(ExcType::NameError, Some(msg)).into()\n    }\n}\n\nimpl From<MontyObject> for ExtFunctionResult {\n    fn from(value: MontyObject) -> Self {\n        Self::Return(value)\n    }\n}\n\nimpl From<MontyException> for ExtFunctionResult {\n    fn from(exception: MontyException) -> Self {\n        Self::Error(exception)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Executor (re-export from run.rs via pub(crate))\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// handle_vm_result\n// ---------------------------------------------------------------------------\n\n/// Pre-converted frame exit data, produced while the VM is still alive.\n///\n/// This intermediate enum holds `MontyObject`s and `String`s instead of `Value`s\n/// and `StringId`s. It exists to separate the conversion phase (needs `&mut VM`)\n/// from the snapshot/progress construction phase (needs owned `Heap`).\npub(crate) enum ConvertedExit {\n    /// Execution completed with a final result.\n    Complete(MontyObject),\n    /// External function call or dataclass method call.\n    FunctionCall {\n        function_name: String,\n        args: Vec<MontyObject>,\n        kwargs: Vec<(MontyObject, MontyObject)>,\n        call_id: u32,\n        method_call: bool,\n    },\n    /// OS-level operation.\n    OsCall {\n        function: OsFunction,\n        args: Vec<MontyObject>,\n        kwargs: Vec<(MontyObject, MontyObject)>,\n        call_id: u32,\n    },\n    /// All async tasks are blocked waiting for external futures.\n    ResolveFutures(Vec<u32>),\n    /// Unresolved name lookup.\n    NameLookup {\n        name: String,\n        namespace_slot: u16,\n        is_global: bool,\n    },\n    /// Runtime error.\n    Error(RunError),\n}\n\nimpl ConvertedExit {\n    /// Returns true if this exit requires a VM snapshot for later resumption.\n    pub(crate) fn needs_snapshot(&self) -> bool {\n        !matches!(self, Self::Complete(_) | Self::Error(_))\n    }\n}\n\n/// Converts a `FrameExit` into a `ConvertedExit` while the VM is still alive.\n///\n/// All `Value` → `MontyObject` and `StringId` → `String` conversions happen here,\n/// while the VM (and its heap/interns) are still accessible.\npub(crate) fn convert_frame_exit(\n    result: RunResult<FrameExit>,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> ConvertedExit {\n    match result {\n        Ok(FrameExit::Return(value)) => ConvertedExit::Complete(MontyObject::new(value, vm)),\n        Ok(FrameExit::ExternalCall {\n            function_name,\n            args,\n            call_id,\n            ..\n        }) => {\n            let name = function_name.into_string(vm.interns);\n            let (args_py, kwargs_py) = args.into_py_objects(vm);\n            ConvertedExit::FunctionCall {\n                function_name: name,\n                args: args_py,\n                kwargs: kwargs_py,\n                call_id: call_id.raw(),\n                method_call: false,\n            }\n        }\n        Ok(FrameExit::OsCall {\n            function,\n            args,\n            call_id,\n        }) => {\n            let (args_py, kwargs_py) = args.into_py_objects(vm);\n            ConvertedExit::OsCall {\n                function,\n                args: args_py,\n                kwargs: kwargs_py,\n                call_id: call_id.raw(),\n            }\n        }\n        Ok(FrameExit::MethodCall {\n            method_name,\n            args,\n            call_id,\n        }) => {\n            let name = method_name.into_string(vm.interns);\n            let (args_py, kwargs_py) = args.into_py_objects(vm);\n            ConvertedExit::FunctionCall {\n                function_name: name,\n                args: args_py,\n                kwargs: kwargs_py,\n                call_id: call_id.raw(),\n                method_call: true,\n            }\n        }\n        Ok(FrameExit::ResolveFutures(pending_call_ids)) => {\n            ConvertedExit::ResolveFutures(pending_call_ids.iter().map(|id| id.raw()).collect())\n        }\n        Ok(FrameExit::NameLookup {\n            name_id,\n            namespace_slot,\n            is_global,\n        }) => {\n            let name = vm.interns.get_str(name_id).to_owned();\n            ConvertedExit::NameLookup {\n                name,\n                namespace_slot,\n                is_global,\n            }\n        }\n        Err(err) => ConvertedExit::Error(err),\n    }\n}\n\n/// Decides whether to snapshot or clean up the VM based on the converted exit.\n///\n/// Consumes the VM. Returns `Some(VMSnapshot)` for suspendable exits, `None` for\n/// completion/error (in which case the VM is cleaned up).\npub(crate) fn check_snapshot_from_converted(\n    converted: &ConvertedExit,\n    mut vm: VM<'_, '_, impl ResourceTracker>,\n) -> Option<VMSnapshot> {\n    if converted.needs_snapshot() {\n        Some(vm.snapshot())\n    } else {\n        vm.cleanup();\n        None\n    }\n}\n\n/// Assembles a `RunProgress` from already-converted data and owned heap.\n///\n/// This runs after the VM has been dropped (releasing the heap borrow),\n/// so the heap can be moved into `Snapshot` structs.\npub(crate) fn build_run_progress<T: ResourceTracker>(\n    converted: ConvertedExit,\n    vm_state: Option<VMSnapshot>,\n    executor: Executor,\n    heap: Heap<T>,\n) -> Result<RunProgress<T>, MontyException> {\n    macro_rules! new_snapshot {\n        () => {\n            Snapshot {\n                executor,\n                vm_state: vm_state.expect(\"snapshot should exist\"),\n                heap,\n            }\n        };\n    }\n\n    match converted {\n        ConvertedExit::Complete(obj) => Ok(RunProgress::Complete(obj)),\n        ConvertedExit::FunctionCall {\n            function_name,\n            args,\n            kwargs,\n            call_id,\n            method_call,\n        } => Ok(RunProgress::FunctionCall(FunctionCall::new(\n            function_name,\n            args,\n            kwargs,\n            call_id,\n            method_call,\n            new_snapshot!(),\n        ))),\n        ConvertedExit::OsCall {\n            function,\n            args,\n            kwargs,\n            call_id,\n        } => Ok(RunProgress::OsCall(OsCall::new(\n            function,\n            args,\n            kwargs,\n            call_id,\n            new_snapshot!(),\n        ))),\n        ConvertedExit::ResolveFutures(pending_call_ids) => Ok(RunProgress::ResolveFutures(ResolveFutures::new(\n            executor,\n            vm_state.expect(\"snapshot should exist for ResolveFutures\"),\n            heap,\n            pending_call_ids,\n        ))),\n        ConvertedExit::NameLookup {\n            name,\n            namespace_slot,\n            is_global,\n        } => Ok(RunProgress::NameLookup(NameLookup::new(\n            name,\n            namespace_slot,\n            is_global,\n            new_snapshot!(),\n        ))),\n        ConvertedExit::Error(err) => Err(err.into_python_exception(&executor.interns, &executor.code)),\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/signature.rs",
    "content": "//! Function signature representation and argument binding.\n//!\n//! This module handles Python function signatures including all parameter types:\n//! positional-only, positional-or-keyword, *args, keyword-only, and **kwargs.\n//! It also handles default values and the argument binding algorithm.\n\nuse crate::{\n    args::{ArgPosIter, ArgValues},\n    bytecode::VM,\n    defer_drop_mut,\n    exception_private::{ExcType, RunResult, SimpleException},\n    expressions::Identifier,\n    heap::{HeapData, HeapGuard},\n    intern::{Interns, StringId},\n    resource::ResourceTracker,\n    types::{Dict, allocate_tuple},\n    value::Value,\n};\n\n/// Represents a Python function signature with all parameter types.\n///\n/// A complete Python signature can include:\n/// - Positional-only parameters (before `/`)\n/// - Positional-or-keyword parameters (regular parameters)\n/// - Variable positional parameter (`*args`)\n/// - Keyword-only parameters (after `*` or `*args`)\n/// - Variable keyword parameter (`**kwargs`)\n///\n/// # Default Values\n///\n/// Default values are tracked by count per parameter group. The `*_defaults_count` fields\n/// indicate how many parameters (from the end of each group) have defaults. For example,\n/// if `args = [a, b, c]` and `arg_defaults_count = 2`, then `b` and `c` have defaults.\n///\n/// Note: The actual default Values are evaluated at function definition time and stored\n/// separately (in the heap as part of the function/closure object). This struct only\n/// tracks the structure, not the values themselves.\n///\n/// # Namespace Layout\n///\n/// Parameters are laid out in the namespace in this order:\n/// ```text\n/// [pos_args][args][*args_slot?][kwargs][**kwargs_slot?]\n/// ```\n/// The `*args` slot is only present if `var_args` is Some.\n/// The `**kwargs` slot is only present if `var_kwargs` is Some.\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]\npub(crate) struct Signature {\n    /// Positional-only parameters, e.g. `a, b` in `def f(a, b, /): ...`\n    ///\n    /// These can only be passed by position, not by keyword.\n    pos_args: Option<Vec<StringId>>,\n\n    /// Number of positional-only parameters with defaults (from the end).\n    pos_defaults_count: usize,\n\n    /// Positional-or-keyword parameters, e.g. `a, b` in `def f(a, b): ...`\n    ///\n    /// These can be passed either by position or by keyword.\n    args: Option<Vec<StringId>>,\n\n    /// Number of positional-or-keyword parameters with defaults (from the end).\n    arg_defaults_count: usize,\n\n    /// Variable positional parameter name, e.g. `args` in `def f(*args): ...`\n    ///\n    /// Collects excess positional arguments into a tuple.\n    var_args: Option<StringId>,\n\n    /// Keyword-only parameters, e.g. `c` in `def f(*, c): ...` or `def f(*args, c): ...`\n    ///\n    /// These can only be passed by keyword, not by position.\n    kwargs: Option<Vec<StringId>>,\n\n    /// Mapping from each keyword-only parameter to its default index (if any).\n    ///\n    /// Each entry corresponds to the same index in `kwargs`. A value of `Some(i)`\n    /// points into the kwarg section of the defaults array, while `None` means\n    /// the parameter is required.\n    kwarg_default_map: Option<Vec<Option<usize>>>,\n\n    /// Variable keyword parameter name, e.g. `kwargs` in `def f(**kwargs): ...`\n    ///\n    /// Collects excess keyword arguments into a dict.\n    var_kwargs: Option<StringId>,\n\n    /// How simple the signature is, used for fast path when binding\n    bind_mode: BindMode,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\nenum BindMode {\n    /// If this is a simple signature (no defaults, no *args/**kwargs).\n    ///\n    /// Simple signatures can use a fast path for argument binding that avoids\n    /// the full binding algorithm overhead. A simple signature has:\n    /// - No positional-only parameters\n    /// - No defaults for any parameters\n    /// - No *args or **kwargs\n    /// - No keyword-only parameters\n    #[default]\n    Simple,\n    /// If this signature has only positional-or-keyword params with defaults.\n    ///\n    /// This identifies the common pattern `def f(a, b=1, c=2)` where:\n    /// - No positional-only parameters\n    /// - No *args or **kwargs\n    /// - No keyword-only parameters\n    /// - Has some default values\n    ///\n    /// These signatures can use a simplified binding that just fills positional\n    /// args and applies defaults without the full algorithm overhead.\n    SimpleWithDefaults,\n    Complex,\n}\n\nimpl Signature {\n    /// Creates a full signature with all parameter types.\n    ///\n    /// # Arguments\n    /// * `pos_args` - Positional-only parameter names\n    /// * `pos_defaults_count` - Number of pos_args with defaults (from end)\n    /// * `args` - Positional-or-keyword parameter names\n    /// * `arg_defaults_count` - Number of args with defaults (from end)\n    /// * `var_args` - Variable positional parameter name (*args)\n    /// * `kwargs` - Keyword-only parameter names\n    /// * `kwarg_default_map` - Mapping of kw-only parameters to default indices\n    /// * `var_kwargs` - Variable keyword parameter name (**kwargs)\n    #[expect(clippy::too_many_arguments)]\n    pub fn new(\n        pos_args: Vec<StringId>,\n        pos_defaults_count: usize,\n        args: Vec<StringId>,\n        arg_defaults_count: usize,\n        var_args: Option<StringId>,\n        kwargs: Vec<StringId>,\n        kwarg_default_map: Vec<Option<usize>>,\n        var_kwargs: Option<StringId>,\n    ) -> Self {\n        let pos_args = if pos_args.is_empty() { None } else { Some(pos_args) };\n        let has_kwonly = !kwargs.is_empty();\n        let kwargs = if has_kwonly { Some(kwargs) } else { None };\n\n        let bind_mode = if pos_args.is_none()\n            && pos_defaults_count == 0\n            && arg_defaults_count == 0\n            && var_args.is_none()\n            && kwargs.is_none()\n            && var_kwargs.is_none()\n        {\n            BindMode::Simple\n        } else if pos_args.is_none()\n            && var_args.is_none()\n            && kwargs.is_none()\n            && var_kwargs.is_none()\n            && arg_defaults_count > 0\n        {\n            BindMode::SimpleWithDefaults\n        } else {\n            BindMode::Complex\n        };\n\n        Self {\n            pos_args,\n            pos_defaults_count,\n            args: if args.is_empty() { None } else { Some(args) },\n            arg_defaults_count,\n            var_args,\n            kwargs,\n            kwarg_default_map: if has_kwonly { Some(kwarg_default_map) } else { None },\n            var_kwargs,\n            bind_mode,\n        }\n    }\n\n    /// Binds arguments to parameters according to Python's calling conventions.\n    ///\n    /// This implements the full argument binding algorithm:\n    /// 1. Bind positional args to pos_args, then args (in order)\n    /// 2. Bind keyword args to args and kwargs (NOT pos_args - positional-only)\n    /// 3. Collect excess positional args into *args tuple\n    /// 4. Collect excess keyword args into **kwargs dict\n    /// 5. Apply defaults for missing parameters\n    ///\n    /// Returns a Vec<Value> ready to be injected into the namespace, laid out as:\n    /// `[pos_args][args][*args_slot?][kwargs][**kwargs_slot?]`\n    ///\n    /// # Arguments\n    /// * `args` - The arguments from the call site\n    /// * `defaults` - Evaluated default values (layout: pos_defaults, arg_defaults, kwarg_defaults)\n    /// * `heap` - The heap for allocating *args tuple and **kwargs dict\n    /// * `interns` - For looking up parameter names in error messages\n    /// * `func_name` - Function name for error messages\n    /// * `namespace` - The namespace to populate with bound arguments. This is mutated in place and will need to be cleaned up on error.\n    ///\n    /// # Errors\n    /// Returns an error if:\n    /// - Too few or too many positional arguments\n    /// - Missing required keyword-only arguments\n    /// - Unexpected keyword argument\n    /// - Positional-only parameter passed as keyword\n    /// - Same argument passed both positionally and by keyword\n    pub fn bind(\n        &self,\n        args: ArgValues,\n        defaults: &[Value],\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        func_name: Identifier,\n        namespace: &mut Vec<Value>,\n    ) -> RunResult<()> {\n        let (pos_iter, keyword_args) = args.into_parts();\n\n        // Convert kwargs to an iterator and guard it so remaining items are cleaned up\n        // on any error path\n        let kwonly_given = keyword_args.len();\n        let keyword_args = keyword_args.into_iter();\n        defer_drop_mut!(keyword_args, vm);\n\n        let namespace_base = namespace.len();\n\n        // Fast path for simple signatures (no defaults, no special params) and\n        // signatures with only positional-or-keyword params and defaults.\n        // This avoids the full binding algorithm overhead for common cases.\n\n        if matches!(self.bind_mode, BindMode::Simple | BindMode::SimpleWithDefaults) && kwonly_given == 0 {\n            match pos_iter {\n                ArgPosIter::Empty => {}\n                ArgPosIter::One(a) => {\n                    namespace.push(a);\n                }\n                ArgPosIter::Two([a1, a2]) => {\n                    namespace.push(a1);\n                    namespace.push(a2);\n                }\n                ArgPosIter::Vec(args) => {\n                    namespace.extend(args);\n                }\n            }\n\n            let actual_count = namespace.len() - namespace_base;\n            let param_count = self.param_count();\n\n            if actual_count == param_count {\n                // Exact match - no defaults needed\n                return Ok(());\n            } else if self.bind_mode == BindMode::SimpleWithDefaults {\n                let required = self.required_positional_count();\n                if actual_count >= required && actual_count < param_count {\n                    // Apply defaults for remaining parameters\n                    // Defaults are stored at the end of the defaults array for pos-or-kw params\n                    let defaults_needed = param_count - actual_count;\n                    let defaults_start = self.arg_defaults_count - defaults_needed;\n                    for i in 0..defaults_needed {\n                        namespace.push(defaults[defaults_start + i].clone_with_heap(vm));\n                    }\n                    return Ok(());\n                }\n            }\n\n            return self.wrong_arg_count_error(actual_count, vm.interns, func_name);\n        }\n\n        // Full binding algorithm for complex signatures or kwargs\n        // Extract interns before guards since HeapGuard borrows the full VM mutably\n        // but we only need mutable access to the heap portion.\n        let mut pos_iter_guard = HeapGuard::new(pos_iter, vm);\n        let (pos_iter, vm) = pos_iter_guard.as_parts_mut();\n\n        // Calculate how many positional params we have\n        let pos_param_count = self.pos_arg_count();\n        let arg_param_count = self.arg_count();\n        let total_positional_params = pos_param_count + arg_param_count;\n\n        // Check positional argument count against maximum\n        if let Some(max) = self.max_positional_count() {\n            let positional_count = pos_iter.len();\n            if positional_count > max {\n                let func = vm.interns.get_str(func_name.name_id);\n                return Err(ExcType::type_error_too_many_positional(\n                    func,\n                    max,\n                    positional_count,\n                    kwonly_given,\n                ));\n            }\n        }\n\n        // Initialize result namespace with Undefined values for all slots\n        // Layout: [pos_args][args][*args?][kwargs][**kwargs?]\n        let var_args_offset = usize::from(self.var_args.is_some());\n        namespace.resize_with(namespace.len() + self.total_slots(), || Value::Undefined);\n\n        // Track which parameters have been bound (for duplicate detection)\n        // Uses a u64 bitmap - supports up to 64 named parameters which is sufficient\n        // for any reasonable Python function (Python itself has practical limits).\n        // Note: this tracks only named params, not *args/**kwargs slots\n        let mut bound_params: u64 = 0;\n\n        // 1. Bind positional args to pos_args, then args\n\n        // Bind to pos_args\n        for (i, slot) in namespace[namespace_base..].iter_mut().enumerate().take(pos_param_count) {\n            if let Some(val) = pos_iter.next() {\n                *slot = val;\n                bound_params |= 1 << i;\n            }\n        }\n\n        // Bind to args\n        for (i, slot) in namespace[namespace_base..]\n            .iter_mut()\n            .enumerate()\n            .take(total_positional_params)\n            .skip(pos_param_count)\n        {\n            if let Some(val) = pos_iter.next() {\n                *slot = val;\n                bound_params |= 1 << i;\n            }\n        }\n\n        // 2. Collect excess positional args into *args tuple\n        if self.var_args.is_some() {\n            namespace[namespace_base + total_positional_params] = allocate_tuple(pos_iter.collect(), vm.heap)?;\n        } else {\n            // If no *args, excess was already checked above via max_positional_count\n            debug_assert_eq!(pos_iter.len(), 0);\n        }\n\n        // 3. Bind keyword args\n        // Bind keywords to args and kwargs (not pos_args - those are positional-only)\n        let mut excess_kwargs_guard = HeapGuard::new(self.var_kwargs.is_some().then(Dict::new), vm);\n        let (excess_kwargs, vm) = excess_kwargs_guard.as_parts_mut();\n\n        'kwargs: for (key, value) in keyword_args {\n            // Guard key: dropped on most paths, consumed into **kwargs via into_parts().\n            let mut key_guard = HeapGuard::new(key, vm);\n            let (key, vm) = key_guard.as_parts_mut();\n\n            // Guard value: consumed into namespace/excess_kwargs via into_inner(),\n            // or dropped automatically on error paths.\n            let mut value_guard = HeapGuard::new(value, vm);\n            let vm = value_guard.heap();\n\n            let Some(keyword_name) = key.as_either_str(vm.heap) else {\n                return Err(ExcType::type_error(\"keywords must be strings\"));\n            };\n\n            // Check if this keyword matches a positional-only param (error)\n            if let Some(pos_args) = &self.pos_args\n                && let Some(&param_id) = pos_args\n                    .iter()\n                    .find(|&&param_id| keyword_name.matches(param_id, vm.interns))\n            {\n                let func = vm.interns.get_str(func_name.name_id);\n                let param = vm.interns.get_str(param_id);\n                return Err(ExcType::type_error_positional_only(func, param));\n            }\n\n            // Try positional-or-keyword params\n            if let Some(args) = &self.args {\n                for (i, &param_id) in args.iter().enumerate() {\n                    if keyword_name.matches(param_id, vm.interns) {\n                        let ns_idx = pos_param_count + i;\n                        if (bound_params & (1 << ns_idx)) != 0 {\n                            let func = vm.interns.get_str(func_name.name_id);\n                            let param = vm.interns.get_str(param_id);\n                            return Err(ExcType::type_error_duplicate_arg(func, param));\n                        }\n                        let (value, _) = value_guard.into_parts();\n                        namespace[namespace_base + ns_idx] = value;\n                        bound_params |= 1 << ns_idx;\n                        continue 'kwargs;\n                    }\n                }\n            }\n\n            // Try keyword-only params\n            if let Some(kwargs) = &self.kwargs {\n                for (i, &param_id) in kwargs.iter().enumerate() {\n                    if keyword_name.matches(param_id, vm.interns) {\n                        let ns_idx = total_positional_params + var_args_offset + i;\n                        let bit_idx = total_positional_params + i;\n                        if (bound_params & (1 << bit_idx)) != 0 {\n                            let func = vm.interns.get_str(func_name.name_id);\n                            let param = vm.interns.get_str(param_id);\n                            return Err(ExcType::type_error_duplicate_arg(func, param));\n                        }\n                        let (value, _) = value_guard.into_parts();\n                        namespace[namespace_base + ns_idx] = value;\n                        bound_params |= 1 << bit_idx;\n                        continue 'kwargs;\n                    }\n                }\n            }\n\n            if let Some(excess_kwargs) = excess_kwargs {\n                // Consume both value and key into **kwargs dict\n                let (value, _) = value_guard.into_parts();\n                let (key, vm) = key_guard.into_parts();\n                excess_kwargs.set(key, value, vm)?;\n                continue 'kwargs;\n            }\n\n            let func = vm.interns.get_str(func_name.name_id);\n            let key_str = keyword_name.as_str(vm.interns);\n            return Err(ExcType::type_error_unexpected_keyword(func, key_str));\n        }\n\n        // 3.5. Apply default values to unbound optional parameters\n        // Defaults layout: [pos_defaults...][arg_defaults...][kwarg_defaults...]\n        // Each section only contains defaults for params that have them.\n        let mut default_idx = 0;\n\n        // Apply pos_args defaults (optional params at the end of pos_args)\n        if self.pos_defaults_count > 0 {\n            let first_optional = pos_param_count - self.pos_defaults_count;\n            for i in first_optional..pos_param_count {\n                if (bound_params & (1 << i)) == 0 {\n                    namespace[namespace_base + i] = defaults[default_idx + (i - first_optional)].clone_with_heap(vm);\n                    bound_params |= 1 << i;\n                }\n            }\n        }\n        default_idx += self.pos_defaults_count;\n\n        // Apply args defaults (optional params at the end of args)\n        if self.arg_defaults_count > 0 {\n            let first_optional = arg_param_count - self.arg_defaults_count;\n            for i in first_optional..arg_param_count {\n                let ns_idx = pos_param_count + i;\n                if (bound_params & (1 << ns_idx)) == 0 {\n                    namespace[namespace_base + ns_idx] =\n                        defaults[default_idx + (i - first_optional)].clone_with_heap(vm);\n                    bound_params |= 1 << ns_idx;\n                }\n            }\n        }\n        default_idx += self.arg_defaults_count;\n\n        // Apply kwargs defaults using the explicit default map\n        if let Some(ref default_map) = self.kwarg_default_map {\n            for (i, default_slot) in default_map.iter().enumerate() {\n                if let Some(slot_idx) = default_slot {\n                    let bound_idx = total_positional_params + i;\n                    // Skip past *args slot if present\n                    let ns_idx = total_positional_params + var_args_offset + i;\n                    if (bound_params & (1 << bound_idx)) == 0 {\n                        namespace[namespace_base + ns_idx] = defaults[default_idx + slot_idx].clone_with_heap(vm);\n                        bound_params |= 1 << bound_idx;\n                    }\n                }\n            }\n        }\n\n        // 4. Check that all required params are bound BEFORE building final namespace.\n        // This ensures we can clean up properly on error without leaking heap values.\n\n        // Check required positional params (pos_args + required args)\n        let mut missing_positional: Vec<&str> = Vec::new();\n\n        // Check pos_args\n        if let Some(ref pos_args) = self.pos_args {\n            let required_pos_only = pos_args.len().saturating_sub(self.pos_defaults_count);\n            for (i, &param_id) in pos_args.iter().enumerate() {\n                if i < required_pos_only && (bound_params & (1 << i)) == 0 {\n                    missing_positional.push(vm.interns.get_str(param_id));\n                }\n            }\n        }\n\n        // Check args (positional-or-keyword)\n        if let Some(ref args_params) = self.args {\n            let required_args = args_params.len().saturating_sub(self.arg_defaults_count);\n            for (i, &param_id) in args_params.iter().enumerate() {\n                if i < required_args && (bound_params & (1 << (pos_param_count + i))) == 0 {\n                    missing_positional.push(vm.interns.get_str(param_id));\n                }\n            }\n        }\n\n        if !missing_positional.is_empty() {\n            // Clean up bound values before returning error\n            let func = vm.interns.get_str(func_name.name_id);\n            return Err(ExcType::type_error_missing_positional_with_names(\n                func,\n                &missing_positional,\n            ));\n        }\n\n        // Check required keyword-only args\n        let mut missing_kwonly: Vec<&str> = Vec::new();\n        if let Some(ref kwargs_params) = self.kwargs {\n            let default_map = self.kwarg_default_map.as_ref();\n            for (i, &param_id) in kwargs_params.iter().enumerate() {\n                let has_default = default_map.and_then(|map| map.get(i)).is_some_and(Option::is_some);\n                if !has_default && (bound_params & (1 << (total_positional_params + i))) == 0 {\n                    missing_kwonly.push(vm.interns.get_str(param_id));\n                }\n            }\n        }\n\n        if !missing_kwonly.is_empty() {\n            let func = vm.interns.get_str(func_name.name_id);\n            return Err(ExcType::type_error_missing_kwonly_with_names(func, &missing_kwonly));\n        }\n\n        // 5. Insert **kwargs dict if present (at the last slot)\n        // Namespace layout: [pos_args][args][*args?][kwargs][**kwargs?]\n        let (excess_kwargs, vm) = excess_kwargs_guard.into_parts();\n        if let Some(excess_kwargs) = excess_kwargs {\n            let dict_id = vm.heap.allocate(HeapData::Dict(excess_kwargs))?;\n            let last_slot = namespace.len() - 1;\n            namespace[last_slot] = Value::Ref(dict_id);\n        }\n\n        Ok(())\n    }\n\n    /// Returns the total number of named parameters (excluding *args/**kwargs slots).\n    ///\n    /// This is `pos_args.len() + args.len() + kwargs.len()`.\n    pub fn param_count(&self) -> usize {\n        self.pos_arg_count() + self.arg_count() + self.kwarg_count()\n    }\n\n    /// Returns the total number of namespace slots needed for parameters.\n    ///\n    /// This includes slots for:\n    /// - All named parameters (pos_args + args + kwargs)\n    /// - The *args tuple (if var_args is Some)\n    /// - The **kwargs dict (if var_kwargs is Some)\n    pub fn total_slots(&self) -> usize {\n        let mut slots = self.param_count();\n        if self.var_args.is_some() {\n            slots += 1;\n        }\n        if self.var_kwargs.is_some() {\n            slots += 1;\n        }\n        slots\n    }\n\n    /// Returns the total number of default values across all parameter groups.\n    pub fn total_defaults_count(&self) -> usize {\n        self.pos_defaults_count + self.arg_defaults_count + self.kwarg_defaults_count()\n    }\n\n    /// Returns the minimum number of positional arguments required.\n    ///\n    /// This is the total positional param count minus the number of defaults.\n    /// For a signature like `def f(a, b, c=1)`, this returns 2 (a and b are required).\n    #[inline]\n    fn required_positional_count(&self) -> usize {\n        self.pos_arg_count() + self.arg_count() - self.pos_defaults_count - self.arg_defaults_count\n    }\n\n    fn kwarg_defaults_count(&self) -> usize {\n        self.kwarg_default_map\n            .as_deref()\n            .map(|v| v.iter().filter(|&x| x.is_some()).count())\n            .unwrap_or_default()\n    }\n\n    /// Returns the number of positional-only parameters.\n    fn pos_arg_count(&self) -> usize {\n        self.pos_args.as_ref().map_or(0, Vec::len)\n    }\n\n    /// Returns the number of positional-or-keyword parameters.\n    fn arg_count(&self) -> usize {\n        self.args.as_ref().map_or(0, Vec::len)\n    }\n\n    /// Returns the number of keyword-only parameters.\n    fn kwarg_count(&self) -> usize {\n        self.kwargs.as_ref().map_or(0, Vec::len)\n    }\n\n    /// Returns an iterator over all parameter names in namespace slot order.\n    ///\n    /// Order: pos_args, args, var_args (if present), kwargs, var_kwargs (if present)\n    fn param_names(&self) -> impl Iterator<Item = StringId> + '_ {\n        let pos_args = self.pos_args.iter().flat_map(|v| v.iter().copied());\n        let args = self.args.iter().flat_map(|v| v.iter().copied());\n        let var_args = self.var_args.iter().copied();\n        let kwargs = self.kwargs.iter().flat_map(|v| v.iter().copied());\n        let var_kwargs = self.var_kwargs.iter().copied();\n\n        pos_args.chain(args).chain(var_args).chain(kwargs).chain(var_kwargs)\n    }\n\n    /// Returns the maximum number of positional arguments accepted.\n    ///\n    /// Returns None if *args is present (unlimited positional args).\n    fn max_positional_count(&self) -> Option<usize> {\n        if self.var_args.is_some() {\n            None\n        } else {\n            Some(self.pos_arg_count() + self.arg_count())\n        }\n    }\n\n    /// Creates an error for wrong number of arguments.\n    ///\n    /// Handles both \"missing required positional arguments\" and \"too many arguments\" cases,\n    /// formatting the error message to match CPython's style.\n    ///\n    /// # Arguments\n    /// * `actual_count` - Number of arguments actually provided\n    /// * `interns` - String storage for looking up interned names\n    fn wrong_arg_count_error<T>(&self, actual_count: usize, interns: &Interns, func_name: Identifier) -> RunResult<T> {\n        let name_str = interns.get_str(func_name.name_id);\n        let param_count = self.param_count();\n        let msg = if let Some(missing_count) = param_count.checked_sub(actual_count) {\n            // Missing arguments - show actual parameter names\n            let mut msg = format!(\n                \"{}() missing {} required positional argument{}: \",\n                name_str,\n                missing_count,\n                if missing_count == 1 { \"\" } else { \"s\" }\n            );\n            // Collect parameter names, skipping the ones already provided\n            let mut missing_names: Vec<_> = self\n                .param_names()\n                .skip(actual_count)\n                .map(|string_id| format!(\"'{}'\", interns.get_str(string_id)))\n                .collect();\n            let last = missing_names.pop().unwrap();\n            if !missing_names.is_empty() {\n                msg.push_str(&missing_names.join(\", \"));\n                msg.push_str(\", and \");\n            }\n            msg.push_str(&last);\n            msg\n        } else {\n            // Too many arguments\n            format!(\n                \"{}() takes {} positional argument{} but {} {} given\",\n                name_str,\n                param_count,\n                if param_count == 1 { \"\" } else { \"s\" },\n                actual_count,\n                if actual_count == 1 { \"was\" } else { \"were\" }\n            )\n        };\n        Err(SimpleException::new_msg(ExcType::TypeError, msg)\n            .with_position(func_name.position)\n            .into())\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/sorting.rs",
    "content": "//! Shared sorting utilities for `sorted()` and `list.sort()`.\n//!\n//! Both `sorted()` and `list.sort()` use index-based sorting: they build\n//! a vector of indices `[0, 1, 2, ...]`, sort the indices by comparing the\n//! corresponding items (or key values), then rearrange items according to\n//! the sorted indices.\n//!\n//! This module provides [`sort_indices`] for the comparison step and\n//! [`apply_permutation`] for the in-place rearrangement step.\n\nuse std::cmp::Ordering;\n\nuse crate::{\n    bytecode::VM,\n    exception_private::{ExcType, RunError},\n    resource::ResourceTracker,\n    types::PyTrait,\n    value::Value,\n};\n\n/// Sorts a vector of indices by comparing items at those positions.\n///\n/// Compares `values[a]` vs `values[b]` using `py_cmp`, optionally reversing\n/// the ordering. If any comparison fails (type error or runtime error), the\n/// sort finishes early and the error is returned.\n///\n/// The `values` slice is typically either the items themselves (no key function)\n/// or the pre-computed key values.\npub fn sort_indices(\n    indices: &mut [usize],\n    values: &[Value],\n    reverse: bool,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<(), RunError> {\n    let mut sort_error: Option<RunError> = None;\n\n    indices.sort_by(|&a, &b| {\n        if sort_error.is_some() {\n            return Ordering::Equal;\n        }\n        if let Err(e) = vm.heap.check_time() {\n            sort_error = Some(e.into());\n            return Ordering::Equal;\n        }\n        match values[a].py_cmp(&values[b], vm) {\n            Ok(Some(ord)) => {\n                if reverse {\n                    ord.reverse()\n                } else {\n                    ord\n                }\n            }\n            Ok(None) => {\n                sort_error = Some(ExcType::type_error(format!(\n                    \"'<' not supported between instances of '{}' and '{}'\",\n                    values[a].py_type(vm.heap),\n                    values[b].py_type(vm.heap)\n                )));\n                Ordering::Equal\n            }\n            Err(e) => {\n                sort_error = Some(e.into());\n                Ordering::Equal\n            }\n        }\n    });\n\n    match sort_error {\n        Some(err) => Err(err),\n        None => Ok(()),\n    }\n}\n\n/// Rearranges `items` in-place according to a permutation of indices.\n///\n/// After calling this, `items[i]` will hold the value that was originally at\n/// `items[indices[i]]`. The algorithm chases permutation cycles and swaps\n/// elements into their final positions, using O(1) extra memory beyond the\n/// `indices` slice (which is mutated to track visited positions).\n///\n/// Each element is moved at most twice (one swap = two moves), so the total\n/// work is O(n) moves. This is at most 2x the moves of building a fresh\n/// `Vec`, but avoids allocating a second buffer.\npub fn apply_permutation(items: &mut [Value], indices: &mut [usize]) {\n    for i in 0..items.len() {\n        if indices[i] == i {\n            continue;\n        }\n        let mut current = i;\n        loop {\n            let target = indices[current];\n            indices[current] = current;\n            if target == i {\n                break;\n            }\n            items.swap(current, target);\n            current = target;\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/bytes.rs",
    "content": "/// Python bytes type, wrapping a `Vec<u8>`.\n///\n/// This type provides Python bytes semantics with operations on ASCII bytes only.\n/// Unlike str methods which operate on Unicode codepoints, bytes methods only\n/// recognize ASCII characters (0-127) for case transformations and predicates.\n///\n/// # Implemented Methods\n///\n/// ## Encoding/Decoding\n/// - `decode([encoding[, errors]])` - Decode to string (UTF-8 only)\n/// - `hex([sep[, bytes_per_sep]])` - Return hex string representation\n/// - `fromhex(string)` - Create bytes from hex string (classmethod)\n///\n/// ## Simple Transformations\n/// - `lower()` - Convert ASCII uppercase to lowercase\n/// - `upper()` - Convert ASCII lowercase to uppercase\n/// - `capitalize()` - First byte uppercase, rest lowercase\n/// - `title()` - Titlecase ASCII letters\n/// - `swapcase()` - Swap ASCII case\n///\n/// ## Predicates\n/// - `isalpha()` - All bytes are ASCII letters\n/// - `isdigit()` - All bytes are ASCII digits\n/// - `isalnum()` - All bytes are ASCII alphanumeric\n/// - `isspace()` - All bytes are ASCII whitespace\n/// - `islower()` - Has cased bytes, all lowercase\n/// - `isupper()` - Has cased bytes, all uppercase\n/// - `isascii()` - All bytes are ASCII (0-127)\n/// - `istitle()` - Titlecased\n///\n/// ## Search Methods\n/// - `count(sub[, start[, end]])` - Count non-overlapping occurrences\n/// - `find(sub[, start[, end]])` - Find first occurrence (-1 if not found)\n/// - `rfind(sub[, start[, end]])` - Find last occurrence (-1 if not found)\n/// - `index(sub[, start[, end]])` - Find first occurrence (raises ValueError)\n/// - `rindex(sub[, start[, end]])` - Find last occurrence (raises ValueError)\n/// - `startswith(prefix[, start[, end]])` - Check if starts with prefix\n/// - `endswith(suffix[, start[, end]])` - Check if ends with suffix\n///\n/// ## Strip/Trim Methods\n/// - `strip([chars])` - Remove leading/trailing bytes\n/// - `lstrip([chars])` - Remove leading bytes\n/// - `rstrip([chars])` - Remove trailing bytes\n/// - `removeprefix(prefix)` - Remove prefix if present\n/// - `removesuffix(suffix)` - Remove suffix if present\n///\n/// ## Split Methods\n/// - `split([sep[, maxsplit]])` - Split on separator\n/// - `rsplit([sep[, maxsplit]])` - Split from right\n/// - `splitlines([keepends])` - Split on line boundaries\n/// - `partition(sep)` - Split into 3 parts at first sep\n/// - `rpartition(sep)` - Split into 3 parts at last sep\n///\n/// ## Replace/Padding Methods\n/// - `replace(old, new[, count])` - Replace occurrences\n/// - `center(width[, fillbyte])` - Center with fill byte\n/// - `ljust(width[, fillbyte])` - Left justify with fill byte\n/// - `rjust(width[, fillbyte])` - Right justify with fill byte\n/// - `zfill(width)` - Pad with zeros\n///\n/// ## Other Methods\n/// - `join(iterable)` - Join bytes sequences\n///\n/// # Unimplemented Methods\n/// - `expandtabs(tabsize=8)` - Tab expansion\n/// - `translate(table[, delete])` - Character translation\n/// - `maketrans(frm, to)` - Create translation table (staticmethod)\nuse std::cmp::Ordering;\nuse std::fmt::Write;\n\nuse ahash::AHashSet;\nuse smallvec::smallvec;\n\nuse super::{MontyIter, PyTrait, Type, str::Str};\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::{DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem},\n    intern::{Interns, StaticStrings, StringId},\n    resource::{ResourceError, ResourceTracker, check_repeat_size, check_replace_size},\n    types::List,\n    value::{EitherStr, Value},\n};\n\n// =============================================================================\n// ASCII byte helper functions\n// =============================================================================\n\n/// Returns true if the byte is Python ASCII whitespace.\n///\n/// Python considers these bytes as whitespace: space, tab, newline, carriage return,\n/// vertical tab (0x0b), and form feed (0x0c). Note: Rust's `is_ascii_whitespace()`\n/// does not include vertical tab (0x0b).\n#[inline]\nfn is_py_whitespace(b: u8) -> bool {\n    matches!(b, b' ' | b'\\t' | b'\\n' | b'\\r' | 0x0b | 0x0c)\n}\n\n/// Gets the byte at a given index, handling negative indices.\n///\n/// Returns `None` if the index is out of bounds.\n/// Negative indices count from the end: -1 is the last byte.\npub fn get_byte_at_index(bytes: &[u8], index: i64) -> Option<u8> {\n    let len = i64::try_from(bytes.len()).ok()?;\n    let normalized = if index < 0 { index + len } else { index };\n\n    if normalized < 0 || normalized >= len {\n        return None;\n    }\n\n    let idx = usize::try_from(normalized).ok()?;\n    Some(bytes[idx])\n}\n\n/// Extracts a slice of a byte array.\n///\n/// Handles both positive and negative step values. For negative step,\n/// iterates backward from start down to (but not including) stop.\n/// The `stop` parameter uses a sentinel value of `len + 1` for negative\n/// step to indicate \"go to the beginning\".\n///\n/// Note: step must be non-zero (callers should validate this via `slice.indices()`).\npub(crate) fn get_bytes_slice(bytes: &[u8], start: usize, stop: usize, step: i64) -> Vec<u8> {\n    let mut result = Vec::new();\n\n    // try_from succeeds for non-negative step; step==0 rejected upstream by slice.indices()\n    if let Ok(step_usize) = usize::try_from(step) {\n        // Positive step: iterate forward\n        let mut i = start;\n        while i < stop && i < bytes.len() {\n            result.push(bytes[i]);\n            i += step_usize;\n        }\n    } else {\n        // Negative step: iterate backward\n        // start is the highest index, stop is the sentinel\n        // stop > bytes.len() means \"go to the beginning\"\n        let step_abs = usize::try_from(-step).expect(\"step is negative so -step is positive\");\n        let step_abs_i64 = i64::try_from(step_abs).expect(\"step magnitude fits in i64\");\n        let mut i = i64::try_from(start).expect(\"start index fits in i64\");\n        let stop_i64 = if stop > bytes.len() {\n            -1\n        } else {\n            i64::try_from(stop).expect(\"stop bounded by bytes.len() fits in i64\")\n        };\n\n        while let Ok(i_usize) = usize::try_from(i) {\n            if i_usize >= bytes.len() || i <= stop_i64 {\n                break;\n            }\n            result.push(bytes[i_usize]);\n            i -= step_abs_i64;\n        }\n    }\n\n    result\n}\n\n/// Python bytes value stored on the heap.\n///\n/// Wraps a `Vec<u8>` and provides Python-compatible operations.\n/// See the module-level documentation for implemented and unimplemented methods.\n#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub(crate) struct Bytes(Vec<u8>);\n\nimpl Bytes {\n    /// Creates a new Bytes from a byte vector.\n    #[must_use]\n    pub fn new(bytes: Vec<u8>) -> Self {\n        Self(bytes)\n    }\n\n    /// Returns a reference to the inner byte slice.\n    #[must_use]\n    pub fn as_slice(&self) -> &[u8] {\n        &self.0\n    }\n\n    /// Creates bytes from the `bytes()` constructor call.\n    ///\n    /// - `bytes()` with no args returns empty bytes\n    /// - `bytes(int)` returns bytes of that length filled with zeros\n    /// - `bytes(string)` encodes the string as UTF-8 (simplified, no encoding param)\n    /// - `bytes(bytes)` returns a copy of the bytes\n    ///\n    /// Note: Full Python semantics for bytes() are more complex (encoding, errors params).\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        let interns = vm.interns;\n        let value = args.get_zero_one_arg(\"bytes\", heap)?;\n        defer_drop!(value, heap);\n        let new_data = match value {\n            None => Vec::new(),\n            Some(Value::Int(n)) => {\n                if *n < 0 {\n                    return Err(ExcType::value_error_negative_bytes_count());\n                }\n                let size = usize::try_from(*n).expect(\"bytes count validated non-negative\");\n                vec![0u8; size]\n            }\n            Some(Value::InternString(string_id)) => {\n                let s = interns.get_str(*string_id);\n                s.as_bytes().to_vec()\n            }\n            Some(Value::InternBytes(bytes_id)) => {\n                let b = interns.get_bytes(*bytes_id);\n                b.to_vec()\n            }\n            Some(v @ Value::Ref(id)) => match heap.get(*id) {\n                HeapData::Str(s) => s.as_str().as_bytes().to_vec(),\n                HeapData::Bytes(b) => b.as_slice().to_vec(),\n                _ => return Err(ExcType::type_error_bytes_init(v.py_type(heap))),\n            },\n            Some(v) => return Err(ExcType::type_error_bytes_init(v.py_type(heap))),\n        };\n        let heap_id = heap.allocate(HeapData::Bytes(Self::new(new_data)))?;\n        Ok(Value::Ref(heap_id))\n    }\n}\n\nimpl From<Vec<u8>> for Bytes {\n    fn from(bytes: Vec<u8>) -> Self {\n        Self(bytes)\n    }\n}\n\nimpl From<&[u8]> for Bytes {\n    fn from(bytes: &[u8]) -> Self {\n        Self(bytes.to_vec())\n    }\n}\n\nimpl From<Bytes> for Vec<u8> {\n    fn from(bytes: Bytes) -> Self {\n        bytes.0\n    }\n}\n\nimpl std::ops::Deref for Bytes {\n    type Target = Vec<u8>;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl PyTrait for Bytes {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Bytes\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.0.len())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        // Check for slice first (Value::Ref pointing to HeapData::Slice)\n        if let Value::Ref(id) = key\n            && let HeapData::Slice(slice) = heap.get(*id)\n        {\n            let (start, stop, step) = slice\n                .indices(self.0.len())\n                .map_err(|()| ExcType::value_error_slice_step_zero())?;\n\n            let sliced_bytes = get_bytes_slice(&self.0, start, stop, step);\n            let heap_id = heap.allocate(HeapData::Bytes(Self::new(sliced_bytes)))?;\n            return Ok(Value::Ref(heap_id));\n        }\n\n        // Extract integer index, accepting Int, Bool (True=1, False=0), and LongInt\n        let index = key.as_index(heap, Type::Bytes)?;\n\n        // Use helper for byte indexing\n        let byte = get_byte_at_index(&self.0, index).ok_or_else(ExcType::bytes_index_error)?;\n        Ok(Value::Int(i64::from(byte)))\n    }\n\n    fn py_eq(&self, other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Ok(self.0 == other.0)\n    }\n\n    fn py_cmp(\n        &self,\n        other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Ordering>, ResourceError> {\n        Ok(Some(self.0.cmp(&other.0)))\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.0.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        bytes_repr_fmt(&self.0, f)\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let Some(method) = attr.static_string() else {\n            args.drop_with_heap(vm.heap);\n            return Err(ExcType::attribute_error(Type::Bytes, attr.as_str(vm.interns)));\n        };\n\n        call_bytes_method_impl(self.as_slice(), method, args, vm).map(CallResult::Value)\n    }\n}\n\nimpl HeapItem for Bytes {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.0.len()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // No-op: bytes don't hold Value references\n    }\n}\n\n/// Calls a bytes method on a byte slice by method name.\n///\n/// This is the entry point for bytes method calls from the VM on interned bytes.\n/// Converts the `StringId` to `StaticStrings` and delegates to `call_bytes_method_impl`.\npub fn call_bytes_method(\n    bytes: &[u8],\n    method_id: StringId,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Value> {\n    let Some(method) = StaticStrings::from_string_id(method_id) else {\n        args.drop_with_heap(vm.heap);\n        return Err(ExcType::attribute_error(Type::Bytes, vm.interns.get_str(method_id)));\n    };\n    call_bytes_method_impl(bytes, method, args, vm)\n}\n\n/// Calls a bytes method on a byte slice.\n///\n/// This is the unified implementation for bytes method calls, used by both\n/// heap-allocated `Bytes` (via `py_call_attr`) and interned bytes literals\n/// (`Value::InternBytes`).\nfn call_bytes_method_impl(\n    bytes: &[u8],\n    method: StaticStrings,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Value> {\n    match method {\n        // Decode method\n        StaticStrings::Decode => bytes_decode(bytes, args, vm),\n        // Simple transformations (no arguments)\n        StaticStrings::Lower => {\n            args.check_zero_args(\"bytes.lower\", vm.heap)?;\n            bytes_lower(bytes, vm)\n        }\n        StaticStrings::Upper => {\n            args.check_zero_args(\"bytes.upper\", vm.heap)?;\n            bytes_upper(bytes, vm)\n        }\n        StaticStrings::Capitalize => {\n            args.check_zero_args(\"bytes.capitalize\", vm.heap)?;\n            bytes_capitalize(bytes, vm)\n        }\n        StaticStrings::Title => {\n            args.check_zero_args(\"bytes.title\", vm.heap)?;\n            bytes_title(bytes, vm)\n        }\n        StaticStrings::Swapcase => {\n            args.check_zero_args(\"bytes.swapcase\", vm.heap)?;\n            bytes_swapcase(bytes, vm)\n        }\n        // Predicate methods (no arguments, return bool)\n        StaticStrings::Isalpha => {\n            args.check_zero_args(\"bytes.isalpha\", vm.heap)?;\n            Ok(Value::Bool(bytes_isalpha(bytes)))\n        }\n        StaticStrings::Isdigit => {\n            args.check_zero_args(\"bytes.isdigit\", vm.heap)?;\n            Ok(Value::Bool(bytes_isdigit(bytes)))\n        }\n        StaticStrings::Isalnum => {\n            args.check_zero_args(\"bytes.isalnum\", vm.heap)?;\n            Ok(Value::Bool(bytes_isalnum(bytes)))\n        }\n        StaticStrings::Isspace => {\n            args.check_zero_args(\"bytes.isspace\", vm.heap)?;\n            Ok(Value::Bool(bytes_isspace(bytes)))\n        }\n        StaticStrings::Islower => {\n            args.check_zero_args(\"bytes.islower\", vm.heap)?;\n            Ok(Value::Bool(bytes_islower(bytes)))\n        }\n        StaticStrings::Isupper => {\n            args.check_zero_args(\"bytes.isupper\", vm.heap)?;\n            Ok(Value::Bool(bytes_isupper(bytes)))\n        }\n        StaticStrings::Isascii => {\n            args.check_zero_args(\"bytes.isascii\", vm.heap)?;\n            Ok(Value::Bool(bytes.iter().all(|&b| b <= 127)))\n        }\n        StaticStrings::Istitle => {\n            args.check_zero_args(\"bytes.istitle\", vm.heap)?;\n            Ok(Value::Bool(bytes_istitle(bytes)))\n        }\n        // Search methods\n        StaticStrings::Count => bytes_count(bytes, args, vm),\n        StaticStrings::Find => bytes_find(bytes, args, vm),\n        StaticStrings::Rfind => bytes_rfind(bytes, args, vm),\n        StaticStrings::Index => bytes_index(bytes, args, vm),\n        StaticStrings::Rindex => bytes_rindex(bytes, args, vm),\n        StaticStrings::Startswith => bytes_startswith(bytes, args, vm),\n        StaticStrings::Endswith => bytes_endswith(bytes, args, vm),\n        // Strip/trim methods\n        StaticStrings::Strip => bytes_strip(bytes, args, vm),\n        StaticStrings::Lstrip => bytes_lstrip(bytes, args, vm),\n        StaticStrings::Rstrip => bytes_rstrip(bytes, args, vm),\n        StaticStrings::Removeprefix => bytes_removeprefix(bytes, args, vm),\n        StaticStrings::Removesuffix => bytes_removesuffix(bytes, args, vm),\n        // Split methods\n        StaticStrings::Split => bytes_split(bytes, args, vm),\n        StaticStrings::Rsplit => bytes_rsplit(bytes, args, vm),\n        StaticStrings::Splitlines => bytes_splitlines(bytes, args, vm),\n        StaticStrings::Partition => bytes_partition(bytes, args, vm),\n        StaticStrings::Rpartition => bytes_rpartition(bytes, args, vm),\n        // Replace/padding methods\n        StaticStrings::Replace => bytes_replace(bytes, args, vm),\n        StaticStrings::Center => bytes_center(bytes, args, vm),\n        StaticStrings::Ljust => bytes_ljust(bytes, args, vm),\n        StaticStrings::Rjust => bytes_rjust(bytes, args, vm),\n        StaticStrings::Zfill => bytes_zfill(bytes, args, vm),\n        // Join method\n        StaticStrings::Join => {\n            let iterable = args.get_one_arg(\"bytes.join\", vm.heap)?;\n            bytes_join(bytes, iterable, vm)\n        }\n        // Hex method\n        StaticStrings::Hex => bytes_hex(bytes, args, vm),\n        // fromhex is a classmethod but also accessible on instances\n        StaticStrings::Fromhex => bytes_fromhex(args, vm),\n        _ => {\n            args.drop_with_heap(vm);\n            Err(ExcType::attribute_error(Type::Bytes, method.into()))\n        }\n    }\n}\n\n/// Writes a CPython-compatible repr string for bytes to a formatter.\n///\n/// Format: `b'...'` or `b\"...\"` depending on content.\n/// - Uses single quotes by default\n/// - Switches to double quotes if bytes contain `'` but not `\"`\n/// - Escapes: `\\\\`, `\\t`, `\\n`, `\\r`, `\\xNN` for non-printable bytes\npub fn bytes_repr_fmt(bytes: &[u8], f: &mut impl Write) -> std::fmt::Result {\n    // Determine quote character: use double quotes if single quote present but not double\n    let has_single = bytes.contains(&b'\\'');\n    let has_double = bytes.contains(&b'\"');\n    let quote = if has_single && !has_double { '\"' } else { '\\'' };\n\n    f.write_char('b')?;\n    f.write_char(quote)?;\n\n    for &byte in bytes {\n        match byte {\n            b'\\\\' => f.write_str(\"\\\\\\\\\")?,\n            b'\\t' => f.write_str(\"\\\\t\")?,\n            b'\\n' => f.write_str(\"\\\\n\")?,\n            b'\\r' => f.write_str(\"\\\\r\")?,\n            b'\\'' if quote == '\\'' => f.write_str(\"\\\\'\")?,\n            b'\"' if quote == '\"' => f.write_str(\"\\\\\\\"\")?,\n            // Printable ASCII (32-126)\n            0x20..=0x7e => f.write_char(byte as char)?,\n            // Non-printable: use \\xNN format\n            _ => write!(f, \"\\\\x{byte:02x}\")?,\n        }\n    }\n\n    f.write_char(quote)\n}\n\n/// Returns a CPython-compatible repr string for bytes.\n///\n/// Convenience wrapper around `bytes_repr_fmt` that returns an owned String.\n#[must_use]\npub fn bytes_repr(bytes: &[u8]) -> String {\n    let mut result = String::new();\n    // Writing to String never fails\n    bytes_repr_fmt(bytes, &mut result).unwrap();\n    result\n}\n\n/// Implements Python's `bytes.decode([encoding[, errors]])` method.\n///\n/// Converts bytes to a string. Currently only supports UTF-8 encoding.\nfn bytes_decode(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (encoding, errors) = args.get_zero_one_two_args(\"bytes.decode\", vm.heap)?;\n    defer_drop!(encoding, vm);\n    defer_drop!(errors, vm); // NB we don't use errors argument yet\n\n    // Check encoding (default UTF-8)\n    let encoding = if let Some(enc) = encoding {\n        get_encoding_str(enc, vm.heap, vm.interns)?.to_ascii_lowercase()\n    } else {\n        \"utf-8\".to_owned()\n    };\n\n    // Only support UTF-8 family\n    if !matches!(encoding.as_str(), \"utf-8\" | \"utf8\" | \"utf_8\") {\n        return Err(ExcType::lookup_error_unknown_encoding(&encoding));\n    }\n\n    // Decode as UTF-8\n    match std::str::from_utf8(bytes) {\n        Ok(s) => {\n            let heap_id = vm.heap.allocate(HeapData::Str(Str::from(s.to_owned())))?;\n            Ok(Value::Ref(heap_id))\n        }\n        Err(_) => Err(ExcType::unicode_decode_error_invalid_utf8()),\n    }\n}\n\n/// Helper function to extract encoding string from a value.\nfn get_encoding_str<'a>(\n    encoding: &Value,\n    heap: &'a Heap<impl ResourceTracker>,\n    interns: &'a Interns,\n) -> RunResult<&'a str> {\n    match encoding {\n        Value::InternString(id) => Ok(interns.get_str(*id)),\n        Value::Ref(id) => match heap.get(*id) {\n            HeapData::Str(s) => Ok(s.as_str()),\n            _ => Err(ExcType::type_error(\n                \"decode() argument 'encoding' must be str, not bytes\",\n            )),\n        },\n        // FIXME: should use proper encoding.py_type() here\n        _ => Err(ExcType::type_error(\"decode() argument 'encoding' must be str, not int\")),\n    }\n}\n\n/// Implements Python's `bytes.count(sub[, start[, end]])` method.\n///\n/// Returns the number of non-overlapping occurrences of the subsequence.\nfn bytes_count(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_bytes_sub_args(\"bytes.count\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let count = if sub.is_empty() {\n        // Empty subsequence: count positions between each byte plus 1\n        slice.len() + 1\n    } else {\n        count_non_overlapping(slice, &sub)\n    };\n\n    let count_i64 = i64::try_from(count).expect(\"count exceeds i64::MAX\");\n    Ok(Value::Int(count_i64))\n}\n\n/// Counts non-overlapping occurrences of needle in haystack.\nfn count_non_overlapping(haystack: &[u8], needle: &[u8]) -> usize {\n    let mut count = 0;\n    let mut pos = 0;\n    while pos + needle.len() <= haystack.len() {\n        if &haystack[pos..pos + needle.len()] == needle {\n            count += 1;\n            pos += needle.len();\n        } else {\n            pos += 1;\n        }\n    }\n    count\n}\n\n/// Implements Python's `bytes.find(sub[, start[, end]])` method.\n///\n/// Returns the lowest index where the subsequence is found, or -1 if not found.\nfn bytes_find(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_bytes_sub_args(\"bytes.find\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let result = if sub.is_empty() {\n        // Empty subsequence: always found at start position\n        Some(0)\n    } else {\n        find_subsequence(slice, &sub)\n    };\n\n    let idx = match result {\n        Some(i) => i64::try_from(start + i).expect(\"index exceeds i64::MAX\"),\n        None => -1,\n    };\n    Ok(Value::Int(idx))\n}\n\n/// Finds the first occurrence of needle in haystack.\nfn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {\n    haystack.windows(needle.len()).position(|window| window == needle)\n}\n\n/// Implements Python's `bytes.index(sub[, start[, end]])` method.\n///\n/// Like find(), but raises ValueError if the subsequence is not found.\nfn bytes_index(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_bytes_sub_args(\"bytes.index\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let result = if sub.is_empty() {\n        // Empty subsequence: always found at start position\n        Some(0)\n    } else {\n        find_subsequence(slice, &sub)\n    };\n\n    match result {\n        Some(i) => {\n            let idx = i64::try_from(start + i).expect(\"index exceeds i64::MAX\");\n            Ok(Value::Int(idx))\n        }\n        None => Err(ExcType::value_error_subsequence_not_found()),\n    }\n}\n\n/// Implements Python's `bytes.startswith(prefix[, start[, end]])` method.\n///\n/// Returns True if bytes starts with the specified prefix.\n/// Accepts bytes or a tuple of bytes as prefix. If a tuple is given, returns True\n/// if any of the prefixes match.\nfn bytes_startswith(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (prefix_arg, start, end) = parse_bytes_prefix_suffix_args(\"bytes.startswith\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let result = match prefix_arg {\n        PrefixSuffixArg::Single(prefix_bytes) => slice.starts_with(&prefix_bytes),\n        PrefixSuffixArg::Multiple(prefixes) => prefixes.iter().any(|p| slice.starts_with(p)),\n    };\n    Ok(Value::Bool(result))\n}\n\n/// Implements Python's `bytes.endswith(suffix[, start[, end]])` method.\n///\n/// Returns True if bytes ends with the specified suffix.\n/// Accepts bytes or a tuple of bytes as suffix. If a tuple is given, returns True\n/// if any of the suffixes match.\nfn bytes_endswith(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (suffix_arg, start, end) = parse_bytes_prefix_suffix_args(\"bytes.endswith\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let result = match suffix_arg {\n        PrefixSuffixArg::Single(suffix_bytes) => slice.ends_with(&suffix_bytes),\n        PrefixSuffixArg::Multiple(suffixes) => suffixes.iter().any(|s| slice.ends_with(s)),\n    };\n    Ok(Value::Bool(result))\n}\n\n/// Argument type for prefix/suffix matching methods.\n///\n/// Represents either a single bytes value or a tuple of bytes values\n/// for matching in startswith/endswith.\nenum PrefixSuffixArg {\n    /// A single bytes value to match\n    Single(Vec<u8>),\n    /// Multiple bytes values to match (from a tuple)\n    Multiple(Vec<Vec<u8>>),\n}\n\n/// Parses arguments for bytes.startswith/endswith methods.\n///\n/// Returns (prefix/suffix_arg, start, end) where start and end are normalized indices.\n/// The prefix/suffix_arg can be a single bytes value or a tuple of bytes values.\n/// Guarantees `start <= end` to prevent slice panics.\nfn parse_bytes_prefix_suffix_args(\n    method: &str,\n    len: usize,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(PrefixSuffixArg, usize, usize)> {\n    let pos = args.into_pos_only(method, vm.heap)?;\n    defer_drop!(pos, vm);\n\n    let (prefix, start, end) = match pos.as_slice() {\n        [prefix_value] => {\n            let prefix = extract_bytes_for_prefix_suffix(prefix_value, method, vm)?;\n            (prefix, 0, len)\n        }\n        [prefix_value, start_value] => {\n            let prefix = extract_bytes_for_prefix_suffix(prefix_value, method, vm)?;\n            let start = normalize_bytes_index(start_value.as_int(vm.heap)?, len);\n            (prefix, start, len)\n        }\n        [prefix_value, start_value, end_value] => {\n            let prefix = extract_bytes_for_prefix_suffix(prefix_value, method, vm)?;\n            let start = normalize_bytes_index(start_value.as_int(vm.heap)?, len);\n            let end = normalize_bytes_index(end_value.as_int(vm.heap)?, len);\n            (prefix, start, end)\n        }\n        [] => return Err(ExcType::type_error_at_least(method, 1, 0)),\n        _ => return Err(ExcType::type_error_at_most(method, 3, pos.len())),\n    };\n\n    // Ensure start <= end to prevent slice panics\n    Ok((prefix, start, end.max(start)))\n}\n\n/// Extracts bytes (or tuple of bytes) for startswith/endswith methods.\n///\n/// Returns `PrefixSuffixArg::Single` for a single bytes value, or\n/// `PrefixSuffixArg::Multiple` for a tuple of bytes values.\nfn extract_bytes_for_prefix_suffix(\n    value: &Value,\n    method: &str,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<PrefixSuffixArg> {\n    // Extract the method name (e.g., \"startswith\" from \"bytes.startswith\")\n    let method_name = method.strip_prefix(\"bytes.\").unwrap_or(method);\n\n    match value {\n        Value::InternBytes(id) => Ok(PrefixSuffixArg::Single(vm.interns.get_bytes(*id).to_vec())),\n        Value::InternString(_) => Err(ExcType::type_error(format!(\n            \"{method_name} first arg must be bytes or a tuple of bytes, not str\"\n        ))),\n        Value::Ref(id) => match vm.heap.get(*id) {\n            HeapData::Bytes(b) => Ok(PrefixSuffixArg::Single(b.as_slice().to_vec())),\n            HeapData::Str(_) => Err(ExcType::type_error(format!(\n                \"{method_name} first arg must be bytes or a tuple of bytes, not str\"\n            ))),\n            HeapData::Tuple(tuple) => {\n                // Extract each element as bytes\n                let items = tuple.as_slice();\n                let mut prefixes = Vec::with_capacity(items.len());\n                for (i, item) in items.iter().enumerate() {\n                    if let Ok(b) = extract_single_bytes_for_prefix_suffix(item, vm.heap, vm.interns) {\n                        prefixes.push(b);\n                    } else {\n                        let item_type = item.py_type(vm.heap);\n                        return Err(ExcType::type_error(format!(\n                            \"{method_name} first arg must be bytes or a tuple of bytes, \\\n                             not tuple containing {item_type} at index {i}\"\n                        )));\n                    }\n                }\n                Ok(PrefixSuffixArg::Multiple(prefixes))\n            }\n            _ => Err(ExcType::type_error(format!(\n                \"{method_name} first arg must be bytes or a tuple of bytes, not {}\",\n                value.py_type(vm.heap)\n            ))),\n        },\n        _ => Err(ExcType::type_error(format!(\n            \"{method_name} first arg must be bytes or a tuple of bytes, not {}\",\n            value.py_type(vm.heap)\n        ))),\n    }\n}\n\n/// Extracts a single bytes value for tuple element in startswith/endswith.\nfn extract_single_bytes_for_prefix_suffix(\n    value: &Value,\n    heap: &Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Vec<u8>> {\n    match value {\n        Value::InternBytes(id) => Ok(interns.get_bytes(*id).to_vec()),\n        Value::InternString(_) => Err(ExcType::type_error(\"expected bytes, not str\")),\n        Value::Ref(id) => match heap.get(*id) {\n            HeapData::Bytes(b) => Ok(b.as_slice().to_vec()),\n            _ => Err(ExcType::type_error(\"expected bytes\")),\n        },\n        _ => Err(ExcType::type_error(\"expected bytes\")),\n    }\n}\n\n/// Extracts bytes from a Value (bytes only, NOT str - matches CPython behavior).\n///\n/// CPython raises `TypeError: a bytes-like object is required, not 'str'` when\n/// a str is passed to bytes methods like find, count, index, startswith, endswith.\nfn extract_bytes_only<'a>(\n    value: &Value,\n    heap: &'a Heap<impl ResourceTracker>,\n    interns: &'a Interns,\n) -> RunResult<&'a [u8]> {\n    match value {\n        Value::InternBytes(id) => Ok(interns.get_bytes(*id)),\n        Value::InternString(_) => Err(ExcType::type_error(\"a bytes-like object is required, not 'str'\")),\n        Value::Ref(id) => match heap.get(*id) {\n            HeapData::Bytes(b) => Ok(b.as_slice()),\n            HeapData::Str(_) => Err(ExcType::type_error(\"a bytes-like object is required, not 'str'\")),\n            _ => Err(ExcType::type_error(\"a bytes-like object is required\")),\n        },\n        _ => Err(ExcType::type_error(\"a bytes-like object is required\")),\n    }\n}\n\n/// Parses arguments for bytes.find/count/index methods.\n///\n/// Returns (sub_bytes, start, end) where start and end are normalized indices.\n/// Guarantees `start <= end` to prevent slice panics.\nfn parse_bytes_sub_args(\n    method: &str,\n    len: usize,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Vec<u8>, usize, usize)> {\n    let pos = args.into_pos_only(method, vm.heap)?;\n    defer_drop!(pos, vm);\n\n    let (sub, start, end) = match pos.as_slice() {\n        [sub_value] => {\n            let sub = extract_bytes_only(sub_value, vm.heap, vm.interns)?;\n            (sub, 0, len)\n        }\n        [sub_value, start_value] => {\n            let sub = extract_bytes_only(sub_value, vm.heap, vm.interns)?;\n            let start = normalize_bytes_index(start_value.as_int(vm.heap)?, len);\n            (sub, start, len)\n        }\n        [sub_value, start_value, end_value] => {\n            let sub = extract_bytes_only(sub_value, vm.heap, vm.interns)?;\n            let start = normalize_bytes_index(start_value.as_int(vm.heap)?, len);\n            let end = normalize_bytes_index(end_value.as_int(vm.heap)?, len);\n            (sub, start, end)\n        }\n        [] => return Err(ExcType::type_error_at_least(method, 1, 0)),\n        _ => return Err(ExcType::type_error_at_most(method, 3, pos.len())),\n    };\n\n    // Ensure start <= end to prevent slice panics (Python treats start > end as empty slice)\n    Ok((sub.to_owned(), start, end.max(start)))\n}\n\n/// Normalizes a Python-style bytes index to a valid index in range [0, len].\nfn normalize_bytes_index(index: i64, len: usize) -> usize {\n    if index < 0 {\n        let abs_index = usize::try_from(-index).unwrap_or(usize::MAX);\n        len.saturating_sub(abs_index)\n    } else {\n        usize::try_from(index).unwrap_or(len).min(len)\n    }\n}\n\n// =============================================================================\n// Simple transformations (no arguments)\n// =============================================================================\n\n/// Implements Python's `bytes.lower()` method.\n///\n/// Returns a copy of the bytes with all ASCII uppercase characters converted to lowercase.\nfn bytes_lower(bytes: &[u8], vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let result: Vec<u8> = bytes.iter().map(|&b| b.to_ascii_lowercase()).collect();\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.upper()` method.\n///\n/// Returns a copy of the bytes with all ASCII lowercase characters converted to uppercase.\nfn bytes_upper(bytes: &[u8], vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let result: Vec<u8> = bytes.iter().map(|&b| b.to_ascii_uppercase()).collect();\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.capitalize()` method.\n///\n/// Returns a copy of the bytes with the first byte capitalized (if ASCII) and\n/// the rest lowercased.\nfn bytes_capitalize(bytes: &[u8], vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let mut result = Vec::with_capacity(bytes.len());\n    if let Some((&first, rest)) = bytes.split_first() {\n        result.push(first.to_ascii_uppercase());\n        for &b in rest {\n            result.push(b.to_ascii_lowercase());\n        }\n    }\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.title()` method.\n///\n/// Returns a titlecased version of the bytes where words start with an uppercase\n/// ASCII character and the remaining characters are lowercase.\nfn bytes_title(bytes: &[u8], vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let mut result = Vec::with_capacity(bytes.len());\n    let mut prev_is_cased = false;\n\n    for &b in bytes {\n        if prev_is_cased {\n            result.push(b.to_ascii_lowercase());\n        } else {\n            result.push(b.to_ascii_uppercase());\n        }\n        prev_is_cased = b.is_ascii_alphabetic();\n    }\n\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.swapcase()` method.\n///\n/// Returns a copy of the bytes with ASCII uppercase characters converted to\n/// lowercase and vice versa.\nfn bytes_swapcase(bytes: &[u8], vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let result: Vec<u8> = bytes\n        .iter()\n        .map(|&b| {\n            if b.is_ascii_uppercase() {\n                b.to_ascii_lowercase()\n            } else if b.is_ascii_lowercase() {\n                b.to_ascii_uppercase()\n            } else {\n                b\n            }\n        })\n        .collect();\n    allocate_bytes(result, vm.heap)\n}\n\n// =============================================================================\n// Predicate methods (no arguments, return bool)\n// =============================================================================\n\n/// Implements Python's `bytes.isalpha()` method.\n///\n/// Returns True if all bytes in the bytes are ASCII letters and there is at least one byte.\nfn bytes_isalpha(bytes: &[u8]) -> bool {\n    !bytes.is_empty() && bytes.iter().all(|&b| b.is_ascii_alphabetic())\n}\n\n/// Implements Python's `bytes.isdigit()` method.\n///\n/// Returns True if all bytes in the bytes are ASCII digits and there is at least one byte.\nfn bytes_isdigit(bytes: &[u8]) -> bool {\n    !bytes.is_empty() && bytes.iter().all(|&b| b.is_ascii_digit())\n}\n\n/// Implements Python's `bytes.isalnum()` method.\n///\n/// Returns True if all bytes in the bytes are ASCII alphanumeric and there is at least one byte.\nfn bytes_isalnum(bytes: &[u8]) -> bool {\n    !bytes.is_empty() && bytes.iter().all(|&b| b.is_ascii_alphanumeric())\n}\n\n/// Implements Python's `bytes.isspace()` method.\n///\n/// Returns True if all bytes in the bytes are ASCII whitespace and there is at least one byte.\nfn bytes_isspace(bytes: &[u8]) -> bool {\n    !bytes.is_empty() && bytes.iter().all(|&b| is_py_whitespace(b))\n}\n\n/// Implements Python's `bytes.islower()` method.\n///\n/// Returns True if all cased bytes are lowercase and there is at least one cased byte.\nfn bytes_islower(bytes: &[u8]) -> bool {\n    let mut has_cased = false;\n    for &b in bytes {\n        if b.is_ascii_uppercase() {\n            return false;\n        }\n        if b.is_ascii_lowercase() {\n            has_cased = true;\n        }\n    }\n    has_cased\n}\n\n/// Implements Python's `bytes.isupper()` method.\n///\n/// Returns True if all cased bytes are uppercase and there is at least one cased byte.\nfn bytes_isupper(bytes: &[u8]) -> bool {\n    let mut has_cased = false;\n    for &b in bytes {\n        if b.is_ascii_lowercase() {\n            return false;\n        }\n        if b.is_ascii_uppercase() {\n            has_cased = true;\n        }\n    }\n    has_cased\n}\n\n/// Implements Python's `bytes.istitle()` method.\n///\n/// Returns True if the bytes are titlecased: uppercase characters follow\n/// uncased characters and lowercase characters follow cased characters.\nfn bytes_istitle(bytes: &[u8]) -> bool {\n    if bytes.is_empty() {\n        return false;\n    }\n\n    let mut prev_cased = false;\n    let mut has_cased = false;\n\n    for &b in bytes {\n        if b.is_ascii_uppercase() {\n            if prev_cased {\n                return false;\n            }\n            prev_cased = true;\n            has_cased = true;\n        } else if b.is_ascii_lowercase() {\n            if !prev_cased {\n                return false;\n            }\n            prev_cased = true;\n            has_cased = true;\n        } else {\n            prev_cased = false;\n        }\n    }\n\n    has_cased\n}\n\n// =============================================================================\n// Search methods\n// =============================================================================\n\n/// Implements Python's `bytes.rfind(sub[, start[, end]])` method.\n///\n/// Returns the highest index where the subsequence is found, or -1 if not found.\nfn bytes_rfind(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_bytes_sub_args(\"bytes.rfind\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let result = if sub.is_empty() {\n        // Empty subsequence: always found at end position\n        Some(slice.len())\n    } else {\n        rfind_subsequence(slice, &sub)\n    };\n\n    let idx = match result {\n        Some(i) => i64::try_from(start + i).expect(\"index exceeds i64::MAX\"),\n        None => -1,\n    };\n    Ok(Value::Int(idx))\n}\n\n/// Finds the last occurrence of needle in haystack.\nfn rfind_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {\n    if needle.len() > haystack.len() {\n        return None;\n    }\n    haystack.windows(needle.len()).rposition(|window| window == needle)\n}\n\n/// Implements Python's `bytes.rindex(sub[, start[, end]])` method.\n///\n/// Like rfind(), but raises ValueError if the subsequence is not found.\nfn bytes_rindex(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_bytes_sub_args(\"bytes.rindex\", bytes.len(), args, vm)?;\n\n    let slice = &bytes[start..end];\n    let result = if sub.is_empty() {\n        Some(slice.len())\n    } else {\n        rfind_subsequence(slice, &sub)\n    };\n\n    match result {\n        Some(i) => {\n            let idx = i64::try_from(start + i).expect(\"index exceeds i64::MAX\");\n            Ok(Value::Int(idx))\n        }\n        None => Err(ExcType::value_error_subsequence_not_found()),\n    }\n}\n\n// =============================================================================\n// Strip/trim methods\n// =============================================================================\n\n/// Implements Python's `bytes.strip([chars])` method.\n///\n/// Returns a copy of the bytes with leading and trailing bytes removed.\n/// If chars is not specified, ASCII whitespace bytes are removed.\nfn bytes_strip(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let value = args.get_zero_one_arg(\"bytes.strip\", vm.heap)?;\n    defer_drop!(value, vm);\n    let result = match value {\n        None | Some(Value::None) => bytes_strip_whitespace_both(bytes),\n        Some(v) => bytes_strip_both(bytes, extract_bytes_only(v, vm.heap, vm.interns)?),\n    };\n    allocate_bytes(result.to_vec(), vm.heap)\n}\n\n/// Implements Python's `bytes.lstrip([chars])` method.\n///\n/// Returns a copy of the bytes with leading bytes removed.\nfn bytes_lstrip(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let value = args.get_zero_one_arg(\"bytes.lstrip\", vm.heap)?;\n    defer_drop!(value, vm);\n    let result = match value {\n        None | Some(Value::None) => bytes_strip_whitespace_start(bytes),\n        Some(v) => bytes_strip_start(bytes, extract_bytes_only(v, vm.heap, vm.interns)?),\n    };\n    allocate_bytes(result.to_vec(), vm.heap)\n}\n\n/// Implements Python's `bytes.rstrip([chars])` method.\n///\n/// Returns a copy of the bytes with trailing bytes removed.\nfn bytes_rstrip(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let value = args.get_zero_one_arg(\"bytes.rstrip\", vm.heap)?;\n    defer_drop!(value, vm);\n    let result = match value {\n        None | Some(Value::None) => bytes_strip_whitespace_end(bytes),\n        Some(v) => bytes_strip_end(bytes, extract_bytes_only(v, vm.heap, vm.interns)?),\n    };\n    allocate_bytes(result.to_vec(), vm.heap)\n}\n\n/// Strips bytes in `chars` from both ends of the byte slice.\nfn bytes_strip_both<'a>(bytes: &'a [u8], chars: &[u8]) -> &'a [u8] {\n    let start = bytes.iter().position(|b| !chars.contains(b)).unwrap_or(bytes.len());\n    let end = bytes\n        .iter()\n        .rposition(|b| !chars.contains(b))\n        .map_or(start, |pos| pos + 1);\n    &bytes[start..end]\n}\n\n/// Strips bytes in `chars` from the start of the byte slice.\nfn bytes_strip_start<'a>(bytes: &'a [u8], chars: &[u8]) -> &'a [u8] {\n    let start = bytes.iter().position(|b| !chars.contains(b)).unwrap_or(bytes.len());\n    &bytes[start..]\n}\n\n/// Strips bytes in `chars` from the end of the byte slice.\nfn bytes_strip_end<'a>(bytes: &'a [u8], chars: &[u8]) -> &'a [u8] {\n    let end = bytes.iter().rposition(|b| !chars.contains(b)).map_or(0, |pos| pos + 1);\n    &bytes[..end]\n}\n\n/// Strips ASCII whitespace from both ends of the byte slice.\nfn bytes_strip_whitespace_both(bytes: &[u8]) -> &[u8] {\n    let start = bytes.iter().position(|b| !is_py_whitespace(*b)).unwrap_or(bytes.len());\n    let end = bytes\n        .iter()\n        .rposition(|b| !is_py_whitespace(*b))\n        .map_or(start, |pos| pos + 1);\n    &bytes[start..end]\n}\n\n/// Strips ASCII whitespace from the start of the byte slice.\nfn bytes_strip_whitespace_start(bytes: &[u8]) -> &[u8] {\n    let start = bytes.iter().position(|b| !is_py_whitespace(*b)).unwrap_or(bytes.len());\n    &bytes[start..]\n}\n\n/// Strips ASCII whitespace from the end of the byte slice.\nfn bytes_strip_whitespace_end(bytes: &[u8]) -> &[u8] {\n    let end = bytes\n        .iter()\n        .rposition(|b| !is_py_whitespace(*b))\n        .map_or(0, |pos| pos + 1);\n    &bytes[..end]\n}\n\n/// Implements Python's `bytes.removeprefix(prefix)` method.\n///\n/// If the bytes start with the prefix, return bytes[len(prefix):].\n/// Otherwise, return a copy of the original bytes.\nfn bytes_removeprefix(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let prefix_value = args.get_one_arg(\"bytes.removeprefix\", vm.heap)?;\n    defer_drop!(prefix_value, vm);\n    let prefix = extract_bytes_only(prefix_value, vm.heap, vm.interns)?;\n\n    let result = if bytes.starts_with(prefix) {\n        bytes[prefix.len()..].to_vec()\n    } else {\n        bytes.to_vec()\n    };\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.removesuffix(suffix)` method.\n///\n/// If the bytes end with the suffix, return bytes[:-len(suffix)].\n/// Otherwise, return a copy of the original bytes.\nfn bytes_removesuffix(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let suffix_value = args.get_one_arg(\"bytes.removesuffix\", vm.heap)?;\n    defer_drop!(suffix_value, vm);\n    let suffix = extract_bytes_only(suffix_value, vm.heap, vm.interns)?;\n\n    let result = if bytes.ends_with(suffix) && !suffix.is_empty() {\n        bytes[..bytes.len() - suffix.len()].to_vec()\n    } else {\n        bytes.to_vec()\n    };\n    allocate_bytes(result, vm.heap)\n}\n\n// =============================================================================\n// Split methods\n// =============================================================================\n\n/// Implements Python's `bytes.split([sep[, maxsplit]])` method.\n///\n/// Returns a list of the bytes split by the separator.\nfn bytes_split(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sep, maxsplit) = parse_bytes_split_args(\"bytes.split\", args, vm)?;\n\n    let parts: Vec<&[u8]> = match &sep {\n        Some(sep) => {\n            if sep.is_empty() {\n                return Err(ExcType::value_error_empty_separator());\n            }\n            if maxsplit < 0 {\n                bytes_split_by_seq(bytes, sep)\n            } else {\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                bytes_splitn_by_seq(bytes, sep, max + 1)\n            }\n        }\n        None => {\n            if maxsplit < 0 {\n                bytes_split_whitespace(bytes)\n            } else {\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                bytes_splitn_whitespace(bytes, max)\n            }\n        }\n    };\n\n    let mut list_items = Vec::with_capacity(parts.len());\n    for part in parts {\n        vm.heap.check_time()?;\n        list_items.push(allocate_bytes(part.to_vec(), vm.heap)?);\n    }\n\n    let list = List::new(list_items);\n    let heap_id = vm.heap.allocate(HeapData::List(list))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Implements Python's `bytes.rsplit([sep[, maxsplit]])` method.\n///\n/// Returns a list of the bytes split by the separator, splitting from the right.\nfn bytes_rsplit(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sep, maxsplit) = parse_bytes_split_args(\"bytes.rsplit\", args, vm)?;\n\n    let parts: Vec<&[u8]> = match &sep {\n        Some(sep) => {\n            if sep.is_empty() {\n                return Err(ExcType::value_error_empty_separator());\n            }\n            if maxsplit < 0 {\n                bytes_split_by_seq(bytes, sep)\n            } else {\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                bytes_rsplitn_by_seq(bytes, sep, max + 1)\n            }\n        }\n        None => {\n            if maxsplit < 0 {\n                bytes_split_whitespace(bytes)\n            } else {\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                bytes_rsplitn_whitespace(bytes, max)\n            }\n        }\n    };\n\n    let mut list_items = Vec::with_capacity(parts.len());\n    for part in parts {\n        vm.heap.check_time()?;\n        list_items.push(allocate_bytes(part.to_vec(), vm.heap)?);\n    }\n\n    let list = List::new(list_items);\n    let heap_id = vm.heap.allocate(HeapData::List(list))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Parses arguments for bytes split methods.\nfn parse_bytes_split_args(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Option<Vec<u8>>, i64)> {\n    let (pos_iter, kwargs) = args.into_parts();\n    defer_drop_mut!(pos_iter, vm);\n    let kwargs_iter = kwargs.into_iter();\n    defer_drop_mut!(kwargs_iter, vm);\n\n    let sep_value = pos_iter.next();\n    defer_drop_mut!(sep_value, vm);\n    let maxsplit_value = pos_iter.next();\n    defer_drop_mut!(maxsplit_value, vm);\n\n    // Check no extra positional arguments\n    if pos_iter.len() != 0 {\n        return Err(ExcType::type_error_at_most(method, 2, 3));\n    }\n\n    // Process keyword arguments\n    for (key, value) in kwargs_iter {\n        defer_drop!(key, vm);\n        let mut value_guard = HeapGuard::new(value, vm);\n\n        let Some(keyword_name) = key.as_either_str(value_guard.heap().heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(value_guard.heap().interns);\n        match key_str {\n            \"sep\" => {\n                if let Some(previous_value) = sep_value.replace(value_guard.into_inner()) {\n                    previous_value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(format!(\n                        \"{method}() got multiple values for argument 'sep'\"\n                    )));\n                }\n            }\n            \"maxsplit\" => {\n                if let Some(previous_value) = maxsplit_value.replace(value_guard.into_inner()) {\n                    previous_value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(format!(\n                        \"{method}() got multiple values for argument 'maxsplit'\"\n                    )));\n                }\n            }\n            _ => {\n                return Err(ExcType::type_error(format!(\n                    \"'{key_str}' is an invalid keyword argument for {method}()\"\n                )));\n            }\n        }\n    }\n\n    // Extract sep (default None)\n    let sep = if let Some(v) = sep_value {\n        if matches!(v, Value::None) {\n            None\n        } else {\n            Some(extract_bytes_only(v, vm.heap, vm.interns)?.to_owned())\n        }\n    } else {\n        None\n    };\n\n    // Extract maxsplit (default -1)\n    let maxsplit = if let Some(v) = maxsplit_value {\n        v.as_int(vm.heap)?\n    } else {\n        -1\n    };\n\n    Ok((sep, maxsplit))\n}\n\n/// Splits bytes by a separator sequence.\nfn bytes_split_by_seq<'a>(bytes: &'a [u8], sep: &[u8]) -> Vec<&'a [u8]> {\n    let mut parts = Vec::new();\n    let mut start = 0;\n\n    while let Some(pos) = find_subsequence(&bytes[start..], sep) {\n        parts.push(&bytes[start..start + pos]);\n        start = start + pos + sep.len();\n    }\n    parts.push(&bytes[start..]);\n\n    parts\n}\n\n/// Splits bytes by a separator sequence, returning at most n parts.\nfn bytes_splitn_by_seq<'a>(bytes: &'a [u8], sep: &[u8], n: usize) -> Vec<&'a [u8]> {\n    let mut parts = Vec::new();\n    let mut start = 0;\n    let mut count = 0;\n\n    while count + 1 < n {\n        if let Some(pos) = find_subsequence(&bytes[start..], sep) {\n            parts.push(&bytes[start..start + pos]);\n            start = start + pos + sep.len();\n            count += 1;\n        } else {\n            break;\n        }\n    }\n    parts.push(&bytes[start..]);\n\n    parts\n}\n\n/// Splits bytes by a separator sequence from the right, returning at most n parts.\nfn bytes_rsplitn_by_seq<'a>(bytes: &'a [u8], sep: &[u8], n: usize) -> Vec<&'a [u8]> {\n    let mut parts = Vec::new();\n    let mut end = bytes.len();\n    let mut count = 0;\n\n    while count + 1 < n {\n        if let Some(pos) = rfind_subsequence(&bytes[..end], sep) {\n            parts.push(&bytes[pos + sep.len()..end]);\n            end = pos;\n            count += 1;\n        } else {\n            break;\n        }\n    }\n    parts.push(&bytes[..end]);\n    parts.reverse();\n\n    parts\n}\n\n/// Splits bytes by ASCII whitespace, filtering empty parts.\nfn bytes_split_whitespace(bytes: &[u8]) -> Vec<&[u8]> {\n    let mut parts = Vec::new();\n    let mut start = None;\n\n    for (i, &b) in bytes.iter().enumerate() {\n        if is_py_whitespace(b) {\n            if let Some(s) = start {\n                parts.push(&bytes[s..i]);\n                start = None;\n            }\n        } else if start.is_none() {\n            start = Some(i);\n        }\n    }\n\n    if let Some(s) = start {\n        parts.push(&bytes[s..]);\n    }\n\n    parts\n}\n\n/// Splits bytes by ASCII whitespace, returning at most maxsplit+1 parts.\nfn bytes_splitn_whitespace(bytes: &[u8], maxsplit: usize) -> Vec<&[u8]> {\n    let mut parts = Vec::new();\n    let mut start = None;\n    let mut count = 0;\n\n    let trimmed = bytes_strip_whitespace_start(bytes);\n    let offset = bytes.len() - trimmed.len();\n\n    for (i, &b) in trimmed.iter().enumerate() {\n        if is_py_whitespace(b) {\n            if let Some(s) = start\n                && count < maxsplit\n            {\n                parts.push(&bytes[offset + s..offset + i]);\n                count += 1;\n                start = None;\n            }\n        } else if start.is_none() {\n            start = Some(i);\n        }\n    }\n\n    if let Some(s) = start {\n        parts.push(&bytes[offset + s..]);\n    }\n\n    parts\n}\n\n/// Splits bytes by ASCII whitespace from the right, returning at most maxsplit+1 parts.\nfn bytes_rsplitn_whitespace(bytes: &[u8], maxsplit: usize) -> Vec<&[u8]> {\n    let mut parts = Vec::new();\n    let mut end = None;\n    let mut count = 0;\n\n    let trimmed = bytes_strip_whitespace_end(bytes);\n\n    for i in (0..trimmed.len()).rev() {\n        let b = trimmed[i];\n        if is_py_whitespace(b) {\n            if let Some(e) = end\n                && count < maxsplit\n            {\n                parts.push(&trimmed[i + 1..e]);\n                count += 1;\n                end = None;\n            }\n        } else if end.is_none() {\n            end = Some(i + 1);\n        }\n    }\n\n    if let Some(e) = end {\n        parts.push(&trimmed[..e]);\n    }\n\n    parts.reverse();\n    parts\n}\n\n/// Implements Python's `bytes.splitlines([keepends])` method.\n///\n/// Returns a list of the lines in the bytes, breaking at line boundaries.\nfn bytes_splitlines(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let keepends = parse_bytes_splitlines_args(args, vm)?;\n\n    let mut lines = Vec::new();\n    let mut start = 0;\n    let len = bytes.len();\n\n    while start < len {\n        vm.heap.check_time()?;\n\n        let mut end = start;\n        let mut line_end = start;\n\n        while end < len {\n            match bytes[end] {\n                b'\\n' => {\n                    line_end = end;\n                    end += 1;\n                    break;\n                }\n                b'\\r' => {\n                    line_end = end;\n                    end += 1;\n                    if end < len && bytes[end] == b'\\n' {\n                        end += 1;\n                    }\n                    break;\n                }\n                _ => {\n                    end += 1;\n                    line_end = end;\n                }\n            }\n        }\n\n        let line = if keepends {\n            &bytes[start..end]\n        } else {\n            &bytes[start..line_end]\n        };\n        lines.push(allocate_bytes(line.to_vec(), vm.heap)?);\n        start = end;\n    }\n\n    let list = List::new(lines);\n    let heap_id = vm.heap.allocate(HeapData::List(list))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Parses arguments for bytes.splitlines method.\nfn parse_bytes_splitlines_args(args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n    let (pos_iter, kwargs) = args.into_parts();\n    defer_drop_mut!(pos_iter, vm);\n    let kwargs = kwargs.into_iter();\n    defer_drop_mut!(kwargs, vm);\n\n    let keepends_value = pos_iter.next();\n    defer_drop_mut!(keepends_value, vm);\n\n    // Check no extra positional arguments\n    if pos_iter.len() != 0 {\n        return Err(ExcType::type_error_at_most(\"bytes.splitlines\", 1, 2));\n    }\n\n    // Process kwargs\n    for (key, value) in kwargs {\n        defer_drop!(key, vm);\n        let mut value_guard = HeapGuard::new(value, vm);\n\n        let Some(keyword_name) = key.as_either_str(value_guard.heap().heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(value_guard.heap().interns);\n        if key_str == \"keepends\" {\n            if let Some(previous_value) = keepends_value.replace(value_guard.into_inner()) {\n                previous_value.drop_with_heap(vm);\n                return Err(ExcType::type_error(\n                    \"bytes.splitlines() got multiple values for argument 'keepends'\",\n                ));\n            }\n        } else {\n            return Err(ExcType::type_error(format!(\n                \"'{key_str}' is an invalid keyword argument for bytes.splitlines()\"\n            )));\n        }\n    }\n\n    // Extract keepends (default false)\n    let keepends = if let Some(v) = keepends_value {\n        v.py_bool(vm)\n    } else {\n        false\n    };\n\n    Ok(keepends)\n}\n\n/// Implements Python's `bytes.partition(sep)` method.\n///\n/// Splits the bytes at the first occurrence of sep, and returns a 3-tuple.\nfn bytes_partition(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let sep_value = args.get_one_arg(\"bytes.partition\", vm.heap)?;\n    defer_drop!(sep_value, vm);\n    let sep = extract_bytes_only(sep_value, vm.heap, vm.interns)?;\n\n    if sep.is_empty() {\n        return Err(ExcType::value_error_empty_separator());\n    }\n\n    let (before, sep_found, after) = match find_subsequence(bytes, sep) {\n        Some(pos) => (bytes[..pos].to_vec(), sep.to_vec(), bytes[pos + sep.len()..].to_vec()),\n        None => (bytes.to_vec(), Vec::new(), Vec::new()),\n    };\n\n    let before_val = allocate_bytes(before, vm.heap)?;\n    let sep_val = allocate_bytes(sep_found, vm.heap)?;\n    let after_val = allocate_bytes(after, vm.heap)?;\n\n    Ok(crate::types::allocate_tuple(\n        smallvec![before_val, sep_val, after_val],\n        vm.heap,\n    )?)\n}\n\n/// Implements Python's `bytes.rpartition(sep)` method.\n///\n/// Splits the bytes at the last occurrence of sep, and returns a 3-tuple.\nfn bytes_rpartition(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let sep_value = args.get_one_arg(\"bytes.rpartition\", vm.heap)?;\n    defer_drop!(sep_value, vm);\n    let sep = extract_bytes_only(sep_value, vm.heap, vm.interns)?;\n\n    if sep.is_empty() {\n        return Err(ExcType::value_error_empty_separator());\n    }\n\n    let (before, sep_found, after) = match rfind_subsequence(bytes, sep) {\n        Some(pos) => (bytes[..pos].to_vec(), sep.to_vec(), bytes[pos + sep.len()..].to_vec()),\n        None => (Vec::new(), Vec::new(), bytes.to_vec()),\n    };\n\n    let before_val = allocate_bytes(before, vm.heap)?;\n    let sep_val = allocate_bytes(sep_found, vm.heap)?;\n    let after_val = allocate_bytes(after, vm.heap)?;\n\n    Ok(crate::types::allocate_tuple(\n        smallvec![before_val, sep_val, after_val],\n        vm.heap,\n    )?)\n}\n\n// =============================================================================\n// Replace/padding methods\n// =============================================================================\n\n/// Implements Python's `bytes.replace(old, new[, count])` method.\n///\n/// Returns a copy with all occurrences of old replaced by new.\nfn bytes_replace(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (old, new, count) = parse_bytes_replace_args(\"bytes.replace\", args, vm)?;\n\n    check_replace_size(bytes.len(), old.len(), new.len(), count, vm.heap.tracker())?;\n\n    let result = if count < 0 {\n        bytes_replace_all(bytes, &old, &new, vm)?\n    } else {\n        let n = usize::try_from(count).unwrap_or(usize::MAX);\n        bytes_replace_n(bytes, &old, &new, n, vm)?\n    };\n\n    allocate_bytes(result, vm.heap)\n}\n\n/// Parses arguments for bytes.replace method.\nfn parse_bytes_replace_args(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Vec<u8>, Vec<u8>, i64)> {\n    let (pos_iter, kwargs) = args.into_parts();\n    defer_drop_mut!(pos_iter, vm);\n    let kwargs_iter = kwargs.into_iter();\n    defer_drop_mut!(kwargs_iter, vm);\n\n    let Some(old_value) = pos_iter.next() else {\n        return Err(ExcType::type_error_at_least(method, 2, 0));\n    };\n    defer_drop!(old_value, vm);\n\n    let Some(new_value) = pos_iter.next() else {\n        return Err(ExcType::type_error_at_least(method, 2, 1));\n    };\n    defer_drop!(new_value, vm);\n\n    let count_value = pos_iter.next();\n    defer_drop_mut!(count_value, vm);\n\n    // Check no extra positional arguments\n    if pos_iter.len() != 0 {\n        return Err(ExcType::type_error_at_most(method, 3, pos_iter.len() + 3));\n    }\n\n    // Process keyword arguments\n    for (key, value) in kwargs_iter {\n        defer_drop!(key, vm);\n        let mut value_guard = HeapGuard::new(value, vm);\n\n        let Some(keyword_name) = key.as_either_str(value_guard.heap().heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(value_guard.heap().interns);\n        match key_str {\n            \"count\" => {\n                if let Some(previous_value) = count_value.replace(value_guard.into_inner()) {\n                    previous_value.drop_with_heap(vm);\n                    return Err(ExcType::type_error(format!(\n                        \"{method}() got multiple values for argument 'count'\"\n                    )));\n                }\n            }\n            _ => {\n                return Err(ExcType::type_error(format!(\n                    \"'{key_str}' is an invalid keyword argument for {method}()\"\n                )));\n            }\n        }\n    }\n\n    // Extract old bytes\n    let old = extract_bytes_only(old_value, vm.heap, vm.interns)?.to_owned();\n\n    // Extract new bytes\n    let new = extract_bytes_only(new_value, vm.heap, vm.interns)?.to_owned();\n\n    // Extract count (default -1)\n    let count = if let Some(v) = count_value {\n        v.as_int(vm.heap)?\n    } else {\n        -1\n    };\n\n    Ok((old, new, count))\n}\n\n/// Replaces all occurrences of `old` with `new` in bytes.\n///\n/// Checks the time limit periodically to enforce `max_duration` during\n/// potentially long replacement operations on large byte sequences.\nfn bytes_replace_all(\n    bytes: &[u8],\n    old: &[u8],\n    new: &[u8],\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<Vec<u8>, ResourceError> {\n    if old.is_empty() {\n        // Empty pattern: insert new before each byte and at the end\n        let mut result = Vec::with_capacity(bytes.len() + new.len() * (bytes.len() + 1));\n        for &b in bytes {\n            vm.heap.check_time()?;\n            result.extend_from_slice(new);\n            result.push(b);\n        }\n        result.extend_from_slice(new);\n        Ok(result)\n    } else {\n        let mut result = Vec::new();\n        let mut start = 0;\n        while let Some(pos) = find_subsequence(&bytes[start..], old) {\n            vm.heap.check_time()?;\n            result.extend_from_slice(&bytes[start..start + pos]);\n            result.extend_from_slice(new);\n            start = start + pos + old.len();\n        }\n        result.extend_from_slice(&bytes[start..]);\n        Ok(result)\n    }\n}\n\n/// Replaces at most n occurrences of `old` with `new` in bytes.\n///\n/// Checks the time limit periodically to enforce `max_duration` during\n/// potentially long replacement operations on large byte sequences.\nfn bytes_replace_n(\n    bytes: &[u8],\n    old: &[u8],\n    new: &[u8],\n    n: usize,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<Vec<u8>, ResourceError> {\n    if old.is_empty() {\n        // Empty pattern: insert new before each byte (up to n times)\n        let mut result = Vec::new();\n        let mut count = 0;\n        for &b in bytes {\n            vm.heap.check_time()?;\n            if count < n {\n                result.extend_from_slice(new);\n                count += 1;\n            }\n            result.push(b);\n        }\n        if count < n {\n            result.extend_from_slice(new);\n        }\n        Ok(result)\n    } else {\n        let mut result = Vec::new();\n        let mut start = 0;\n        let mut count = 0;\n        while count < n {\n            vm.heap.check_time()?;\n            if let Some(pos) = find_subsequence(&bytes[start..], old) {\n                result.extend_from_slice(&bytes[start..start + pos]);\n                result.extend_from_slice(new);\n                start = start + pos + old.len();\n                count += 1;\n            } else {\n                break;\n            }\n        }\n        result.extend_from_slice(&bytes[start..]);\n        Ok(result)\n    }\n}\n\n/// Implements Python's `bytes.center(width[, fillbyte])` method.\n///\n/// Returns centered in a bytes of length width.\nfn bytes_center(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (width, fillbyte) = parse_bytes_justify_args(\"bytes.center\", args, vm)?;\n    let len = bytes.len();\n\n    let result = if width <= len {\n        bytes.to_vec()\n    } else {\n        check_repeat_size(width, 1, vm.heap.tracker())?;\n        let total_pad = width - len;\n        let left_pad = total_pad / 2;\n        let right_pad = total_pad - left_pad;\n        let mut result = Vec::with_capacity(width);\n        for _ in 0..left_pad {\n            result.push(fillbyte);\n        }\n        result.extend_from_slice(bytes);\n        for _ in 0..right_pad {\n            result.push(fillbyte);\n        }\n        result\n    };\n\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.ljust(width[, fillbyte])` method.\n///\n/// Returns left-justified in a bytes of length width.\nfn bytes_ljust(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (width, fillbyte) = parse_bytes_justify_args(\"bytes.ljust\", args, vm)?;\n    let len = bytes.len();\n\n    let result = if width <= len {\n        bytes.to_vec()\n    } else {\n        check_repeat_size(width, 1, vm.heap.tracker())?;\n        let pad = width - len;\n        let mut result = Vec::with_capacity(width);\n        result.extend_from_slice(bytes);\n        for _ in 0..pad {\n            result.push(fillbyte);\n        }\n        result\n    };\n\n    allocate_bytes(result, vm.heap)\n}\n\n/// Implements Python's `bytes.rjust(width[, fillbyte])` method.\n///\n/// Returns right-justified in a bytes of length width.\nfn bytes_rjust(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (width, fillbyte) = parse_bytes_justify_args(\"bytes.rjust\", args, vm)?;\n    let len = bytes.len();\n\n    let result = if width <= len {\n        bytes.to_vec()\n    } else {\n        check_repeat_size(width, 1, vm.heap.tracker())?;\n        let pad = width - len;\n        let mut result = Vec::with_capacity(width);\n        for _ in 0..pad {\n            result.push(fillbyte);\n        }\n        result.extend_from_slice(bytes);\n        result\n    };\n\n    allocate_bytes(result, vm.heap)\n}\n\n/// Parses arguments for bytes justify methods (center, ljust, rjust).\nfn parse_bytes_justify_args(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(usize, u8)> {\n    let pos = args.into_pos_only(method, vm.heap)?;\n    defer_drop!(pos, vm);\n\n    let extract_width = |v: &Value| -> RunResult<usize> {\n        let w = v.as_int(vm.heap)?;\n        Ok(if w < 0 {\n            0\n        } else {\n            usize::try_from(w).unwrap_or(usize::MAX)\n        })\n    };\n\n    let extract_fill = |v: &Value| -> RunResult<u8> {\n        let fill_bytes = extract_bytes_only(v, vm.heap, vm.interns)?;\n        if fill_bytes.len() != 1 {\n            return Err(ExcType::type_error(format!(\n                \"{method}() argument 2 must be a byte string of length 1, not bytes of length {}\",\n                fill_bytes.len()\n            )));\n        }\n        Ok(fill_bytes[0])\n    };\n\n    match pos.as_slice() {\n        [width_value] => Ok((extract_width(width_value)?, b' ')),\n        [width_value, fillbyte_value] => Ok((extract_width(width_value)?, extract_fill(fillbyte_value)?)),\n        [] => Err(ExcType::type_error_at_least(method, 1, 0)),\n        _ => Err(ExcType::type_error_at_most(method, 2, pos.len())),\n    }\n}\n\n/// Implements Python's `bytes.zfill(width)` method.\n///\n/// Returns a copy of the bytes left filled with ASCII '0' digits.\nfn bytes_zfill(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let width_value = args.get_one_arg(\"bytes.zfill\", vm.heap)?;\n    defer_drop!(width_value, vm);\n    let width_i64 = width_value.as_int(vm.heap)?;\n\n    let width = if width_i64 < 0 {\n        0\n    } else {\n        usize::try_from(width_i64).unwrap_or(usize::MAX)\n    };\n    let len = bytes.len();\n\n    let result = if width <= len {\n        bytes.to_vec()\n    } else {\n        check_repeat_size(width, 1, vm.heap.tracker())?;\n        let pad = width - len;\n        let mut result = Vec::with_capacity(width);\n\n        // Handle sign prefix\n        if !bytes.is_empty() && (bytes[0] == b'+' || bytes[0] == b'-') {\n            result.push(bytes[0]);\n            result.resize(pad + 1, b'0');\n            result.extend_from_slice(&bytes[1..]);\n        } else {\n            result.resize(pad, b'0');\n            result.extend_from_slice(bytes);\n        }\n        result\n    };\n\n    allocate_bytes(result, vm.heap)\n}\n\n// =============================================================================\n// Join method\n// =============================================================================\n\n/// Implements Python's `bytes.join(iterable)` method.\n///\n/// Joins elements of the iterable with the separator bytes.\nfn bytes_join(separator: &[u8], iterable: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let Ok(iter) = MontyIter::new(iterable, vm) else {\n        return Err(ExcType::type_error_join_not_iterable());\n    };\n    defer_drop_mut!(iter, vm);\n\n    let mut result = Vec::new();\n    let mut index = 0usize;\n\n    while let Some(item) = iter.for_next(vm)? {\n        defer_drop!(item, vm);\n\n        if index > 0 {\n            result.extend_from_slice(separator);\n        }\n\n        // Check item is bytes and extract its content\n        match item {\n            Value::InternBytes(id) => {\n                result.extend_from_slice(vm.interns.get_bytes(*id));\n            }\n            Value::Ref(heap_id) => {\n                if let HeapData::Bytes(b) = vm.heap.get(*heap_id) {\n                    result.extend_from_slice(b.as_slice());\n                } else {\n                    let t = item.py_type(vm.heap);\n                    return Err(ExcType::type_error(format!(\n                        \"sequence item {index}: expected a bytes-like object, {t} found\"\n                    )));\n                }\n            }\n            _ => {\n                let t = item.py_type(vm.heap);\n                return Err(ExcType::type_error(format!(\n                    \"sequence item {index}: expected a bytes-like object, {t} found\"\n                )));\n            }\n        }\n        index += 1;\n    }\n\n    allocate_bytes(result, vm.heap)\n}\n\n// =============================================================================\n// Hex method\n// =============================================================================\n\n/// Implements Python's `bytes.hex([sep[, bytes_per_sep]])` method.\n///\n/// Returns a string containing the hexadecimal representation of the bytes.\nfn bytes_hex(bytes: &[u8], args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sep, bytes_per_sep) = parse_bytes_hex_args(args, vm)?;\n\n    let hex_chars: Vec<char> = bytes\n        .iter()\n        .flat_map(|b| {\n            let hi = (b >> 4) & 0xf;\n            let lo = b & 0xf;\n            let hi_char = if hi < 10 {\n                (b'0' + hi) as char\n            } else {\n                (b'a' + hi - 10) as char\n            };\n            let lo_char = if lo < 10 {\n                (b'0' + lo) as char\n            } else {\n                (b'a' + lo - 10) as char\n            };\n            [hi_char, lo_char]\n        })\n        .collect();\n\n    let result = if let Some(sep) = sep {\n        if bytes_per_sep == 0 || bytes.is_empty() {\n            hex_chars.iter().collect()\n        } else {\n            // Insert separator every `bytes_per_sep` bytes (2*bytes_per_sep hex chars)\n            let chars_per_group = usize::try_from(bytes_per_sep.unsigned_abs()).unwrap_or(usize::MAX) * 2;\n            let mut result = String::new();\n\n            if bytes_per_sep > 0 {\n                // Positive: count from right, so partial group is at the START\n                let total_len = hex_chars.len();\n                let first_chunk_len = total_len % chars_per_group;\n                let first_chunk_len = if first_chunk_len == 0 {\n                    chars_per_group\n                } else {\n                    first_chunk_len\n                };\n\n                result.extend(&hex_chars[..first_chunk_len]);\n                for chunk in hex_chars[first_chunk_len..].chunks(chars_per_group) {\n                    result.push(sep);\n                    result.extend(chunk);\n                }\n            } else {\n                // Negative: count from left, so partial group is at the END\n                for (i, chunk) in hex_chars.chunks(chars_per_group).enumerate() {\n                    if i > 0 {\n                        result.push(sep);\n                    }\n                    result.extend(chunk);\n                }\n            }\n            result\n        }\n    } else {\n        hex_chars.iter().collect()\n    };\n\n    crate::types::str::allocate_string(result, vm.heap)\n}\n\n/// Parses arguments for bytes.hex method.\nfn parse_bytes_hex_args(args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<(Option<char>, i64)> {\n    let pos = args.into_pos_only(\"bytes.hex\", vm.heap)?;\n    defer_drop!(pos, vm);\n\n    let (sep_value, bps_value) = match pos.as_slice() {\n        [] => return Ok((None, 1)),\n        [sep_value] => (sep_value, None),\n        [sep_value, bps_value] => (sep_value, Some(bps_value)),\n        other => return Err(ExcType::type_error_at_most(\"bytes.hex\", 2, other.len())),\n    };\n\n    let sep_bytes = match sep_value {\n        Value::InternString(id) => vm.interns.get_str(*id).as_bytes(),\n        Value::InternBytes(id) => vm.interns.get_bytes(*id),\n        Value::Ref(heap_id) => match vm.heap.get(*heap_id) {\n            HeapData::Str(s) => s.as_bytes(),\n            HeapData::Bytes(b) => b.as_slice(),\n            _ => return Err(ExcType::type_error(\"sep must be str or bytes\")),\n        },\n        _ => return Err(ExcType::type_error(\"sep must be str or bytes\")),\n    };\n\n    let sep = match sep_bytes {\n        [b] if b.is_ascii() => *b as char,\n        _ => return Err(SimpleException::new_msg(ExcType::ValueError, \"sep must be a single ASCII character\").into()),\n    };\n\n    let bytes_per_sep = if let Some(bps_value) = bps_value {\n        bps_value.as_int(vm.heap)?\n    } else {\n        1\n    };\n\n    Ok((Some(sep), bytes_per_sep))\n}\n\n// =============================================================================\n// fromhex classmethod\n// =============================================================================\n\n/// Implements Python's `bytes.fromhex(string)` classmethod.\n///\n/// Creates bytes from a hexadecimal string. Whitespace is allowed between byte pairs,\n/// but not between the two digits of a byte.\npub fn bytes_fromhex(args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let hex_value = args.get_one_arg(\"bytes.fromhex\", vm.heap)?;\n    defer_drop!(hex_value, vm);\n\n    let hex_str = match hex_value {\n        Value::InternString(id) => vm.interns.get_str(*id),\n        Value::Ref(heap_id) => {\n            if let HeapData::Str(s) = vm.heap.get(*heap_id) {\n                s.as_str()\n            } else {\n                return Err(ExcType::type_error(\"fromhex() argument must be str, not bytes\"));\n            }\n        }\n        _ => {\n            let t = hex_value.py_type(vm.heap);\n            return Err(ExcType::type_error(format!(\"fromhex() argument must be str, not {t}\")));\n        }\n    };\n\n    // CPython allows whitespace BETWEEN byte pairs, but NOT within a pair.\n    // - \"de ad\" is valid (whitespace between pairs)\n    // - \"d e\" or \"0 1\" are NOT valid (whitespace within a pair)\n    // - \" 01 \" is valid (whitespace before/after)\n    //\n    // Error messages:\n    // - Invalid char (including whitespace in wrong place): \"non-hexadecimal number found ... at position X\"\n    // - Odd number of valid hex digits: \"must contain an even number of hexadecimal digits\"\n\n    let mut result = Vec::new();\n    let mut chars = hex_str.chars().enumerate().peekable();\n\n    loop {\n        // Skip whitespace BETWEEN byte pairs (before the high nibble)\n        while chars.peek().is_some_and(|(_, c)| c.is_whitespace()) {\n            chars.next();\n        }\n\n        // Get high nibble\n        let Some((hi_pos, hi_char)) = chars.next() else {\n            break; // End of string - we're done\n        };\n\n        let Some(hi_val) = hex_char_to_value(hi_char) else {\n            return Err(SimpleException::new_msg(\n                ExcType::ValueError,\n                format!(\"non-hexadecimal number found in fromhex() arg at position {hi_pos}\"),\n            )\n            .into());\n        };\n\n        // Get low nibble - must be IMMEDIATELY after high nibble (no whitespace)\n        let Some((lo_pos, lo_char)) = chars.next() else {\n            // End of string after high nibble = odd number of hex digits\n            return Err(SimpleException::new_msg(\n                ExcType::ValueError,\n                \"fromhex() arg must contain an even number of hexadecimal digits\",\n            )\n            .into());\n        };\n\n        let Some(lo_val) = hex_char_to_value(lo_char) else {\n            // Invalid character (including whitespace) in low nibble position\n            return Err(SimpleException::new_msg(\n                ExcType::ValueError,\n                format!(\"non-hexadecimal number found in fromhex() arg at position {lo_pos}\"),\n            )\n            .into());\n        };\n\n        result.push((hi_val << 4) | lo_val);\n    }\n\n    allocate_bytes(result, vm.heap)\n}\n\n/// Converts a hex character to its numeric value.\nfn hex_char_to_value(c: char) -> Option<u8> {\n    match c {\n        '0'..='9' => Some(c as u8 - b'0'),\n        'a'..='f' => Some(c as u8 - b'a' + 10),\n        'A'..='F' => Some(c as u8 - b'A' + 10),\n        _ => None,\n    }\n}\n\n// =============================================================================\n// Helper function for bytes allocation\n// =============================================================================\n\n/// Allocates bytes on the heap.\nfn allocate_bytes(bytes: Vec<u8>, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    let heap_id = heap.allocate(HeapData::Bytes(Bytes::new(bytes)))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/types/dataclass.rs",
    "content": "use std::fmt::Write;\n\nuse ahash::AHashSet;\n\nuse super::{Dict, PyTrait};\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapId, HeapItem},\n    intern::Interns,\n    resource::{ResourceError, ResourceTracker},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Python dataclass instance type.\n///\n/// Represents an instance of a dataclass with a class name, field values, and\n/// frozen/mutable semantics. Method calls on dataclasses are detected lazily:\n/// when `call_attr` is invoked on a dataclass and the attribute name is not found\n/// in `attrs`, it is dispatched as a `MethodCall` to the host (provided the name\n/// is public — no leading underscore).\n///\n/// # Fields\n/// - `name`: The class name (e.g., \"Point\", \"User\")\n/// - `field_names`: Declared field names in definition order (used for repr)\n/// - `attrs`: All attributes including declared fields and dynamically added ones\n/// - `frozen`: Whether the dataclass instance is immutable\n///\n/// # Hashability\n/// When `frozen` is true, the dataclass is immutable and hashable. The hash\n/// is computed from the class name and declared field values only.\n/// When `frozen` is false, the dataclass is mutable and unhashable.\n///\n/// # Reference Counting\n/// The `attrs` Dict contains Values that may be heap-allocated. The\n/// `py_dec_ref_ids` method properly handles decrementing refcounts for\n/// all attribute values when the dataclass instance is freed.\n///\n/// # Attribute Access\n/// - Getting: Looks up the attribute name in the attrs Dict\n/// - Setting: Updates or adds the attribute in attrs (only if not frozen)\n/// - Method calls: If the attribute is a public name not found in attrs, dispatched to host\n/// - repr: Only shows declared fields (from field_names), not extra attributes\n#[derive(Debug)]\npub(crate) struct Dataclass {\n    /// The class name (e.g., \"Point\", \"User\")\n    name: EitherStr,\n    /// Identifier of the type, from `id(type(dc))` in python.\n    type_id: u64,\n    /// Declared field names in definition order (for repr and hashing)\n    field_names: Vec<String>,\n    /// All attributes (both declared fields and dynamically added)\n    attrs: Dict,\n    /// Whether this dataclass instance is immutable (affects hashability)\n    frozen: bool,\n}\n\nimpl Dataclass {\n    /// Creates a new dataclass instance.\n    ///\n    /// # Arguments\n    /// * `name` - The class name\n    /// * `type_id` - The type ID of the dataclass\n    /// * `field_names` - Declared field names in definition order\n    /// * `attrs` - Dict of attribute name -> value pairs (ownership transferred)\n    /// * `frozen` - Whether this dataclass instance is immutable (affects hashability)\n    #[must_use]\n    pub fn new(name: impl Into<EitherStr>, type_id: u64, field_names: Vec<String>, attrs: Dict, frozen: bool) -> Self {\n        Self {\n            name: name.into(),\n            type_id,\n            field_names,\n            attrs,\n            frozen,\n        }\n    }\n\n    /// Returns the class name.\n    #[must_use]\n    pub fn name<'a>(&'a self, interns: &'a Interns) -> &'a str {\n        self.name.as_str(interns)\n    }\n\n    /// Returns the type ID of the dataclass.\n    #[must_use]\n    pub fn type_id(&self) -> u64 {\n        self.type_id\n    }\n\n    /// Returns a reference to the declared field names.\n    #[must_use]\n    pub fn field_names(&self) -> &[String] {\n        &self.field_names\n    }\n\n    /// Returns whether this dataclass contains any heap references (`Value::Ref`).\n    ///\n    /// Delegates to the underlying attrs Dict.\n    #[inline]\n    #[must_use]\n    pub fn has_refs(&self) -> bool {\n        self.attrs.has_refs()\n    }\n\n    /// Returns a reference to the attrs Dict.\n    #[must_use]\n    pub fn attrs(&self) -> &Dict {\n        &self.attrs\n    }\n\n    /// Returns whether this dataclass instance is frozen (immutable).\n    #[must_use]\n    pub fn is_frozen(&self) -> bool {\n        self.frozen\n    }\n\n    /// Sets an attribute value.\n    ///\n    /// The caller transfers ownership of both `name` and `value`. Returns the\n    /// old value if the attribute existed (caller must drop it), or None if this\n    /// is a new attribute.\n    ///\n    /// Returns `FrozenInstanceError` if the dataclass is frozen.\n    pub fn set_attr(\n        &mut self,\n        name: Value,\n        value: Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Option<Value>> {\n        if self.frozen {\n            // Get attribute name for error message\n            let attr_name = match &name {\n                Value::InternString(id) => vm.interns.get_str(*id).to_string(),\n                _ => \"<unknown>\".to_string(),\n            };\n            // Drop the values we were given ownership of\n            name.drop_with_heap(vm);\n            value.drop_with_heap(vm);\n            return Err(ExcType::frozen_instance_error(&attr_name));\n        }\n        self.attrs.set(name, value, vm)\n    }\n\n    /// Computes the hash for this dataclass if it's frozen.\n    ///\n    /// Returns `Ok(Some(hash))` for frozen (immutable) dataclasses, `Ok(None)` for mutable ones.\n    /// Returns `Err(ResourceError::Recursion)` if the recursion limit is exceeded.\n    /// The hash is computed from the class name and declared field values only.\n    pub fn compute_hash(\n        &self,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> Result<Option<u64>, ResourceError> {\n        use std::{\n            collections::hash_map::DefaultHasher,\n            hash::{Hash, Hasher},\n        };\n\n        // Only frozen (immutable) dataclasses are hashable\n        if !self.frozen {\n            return Ok(None);\n        }\n\n        let token = heap.incr_recursion_depth()?;\n        defer_drop!(token, heap);\n        let mut hasher = DefaultHasher::new();\n        // Hash the class name\n        self.name.hash(&mut hasher);\n        // Hash each declared field (name, value) pair in order\n        for field_name in &self.field_names {\n            field_name.hash(&mut hasher);\n            if let Some(value) = self.attrs.get_by_str(field_name, heap, interns) {\n                match value.py_hash(heap, interns)? {\n                    Some(h) => h.hash(&mut hasher),\n                    None => return Ok(None),\n                }\n            }\n        }\n        Ok(Some(hasher.finish()))\n    }\n}\n\nimpl PyTrait for Dataclass {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Dataclass\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        // Dataclasses don't have a length\n        None\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        // Dataclasses are equal if they have the same name and equal attrs\n        Ok(self.name == other.name && self.attrs.py_eq(&other.attrs, vm)?)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        // Dataclass instances are always truthy (like Python objects)\n        true\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        // Check depth limit before recursing\n        let heap = &*vm.heap;\n        let Some(token) = heap.incr_recursion_depth_for_repr() else {\n            return f.write_str(\"...\");\n        };\n        crate::defer_drop_immutable_heap!(token, heap);\n\n        // Format: ClassName(field1=value1, field2=value2, ...)\n        // Only declared fields are shown, not dynamically added attributes\n        f.write_str(self.name(vm.interns))?;\n        f.write_char('(')?;\n\n        let mut first = true;\n        for field_name in &self.field_names {\n            if !first {\n                f.write_str(\", \")?;\n            }\n            first = false;\n\n            // Write field name\n            f.write_str(field_name)?;\n            f.write_char('=')?;\n\n            // Look up value in attrs\n            if let Some(value) = self.attrs.get_by_str(field_name, heap, vm.interns) {\n                value.py_repr_fmt(f, vm, heap_ids)?;\n            } else {\n                // Field not found - shouldn't happen for well-formed dataclasses\n                f.write_str(\"<?>\")?;\n            }\n        }\n\n        f.write_char(')')?;\n        Ok(())\n    }\n\n    /// Performs lazy method detection for dataclass instances.\n    ///\n    /// If the attribute is a public name (no leading underscore) not found in the\n    /// dataclass's attrs dict, returns `MethodCall` so the VM yields to the host.\n    /// Otherwise handles the call directly:\n    /// - Attributes that exist in attrs but aren't callable produce `TypeError`\n    /// - Private/dunder attributes that aren't in attrs produce `AttributeError`\n    fn py_call_attr(\n        &mut self,\n        self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let heap = &mut *vm.heap;\n        let interns = vm.interns;\n        let attr_str = attr.as_str(interns);\n        // Only public methods (no underscore prefix = no dunders, no private)\n        if !attr_str.starts_with('_') && self.attrs.get_by_str(attr_str, heap, interns).is_none() {\n            // Clone self and prepend to args for the method call\n            // inc_ref works even when data is taken out (refcount metadata is separate)\n            heap.inc_ref(self_id);\n            let self_arg = Value::Ref(self_id);\n            let args_with_self = args.prepend(self_arg);\n            Ok(CallResult::MethodCall(attr.clone(), args_with_self))\n        } else {\n            // Not a method call — handle directly\n            let method_name = attr.as_str(interns);\n            defer_drop!(args, heap);\n\n            // If the attribute exists in attrs, it's a data value (not callable)\n            if let Some(value) = self.attrs.get_by_str(method_name, heap, interns) {\n                let type_name = value.py_type(heap);\n                Err(ExcType::type_error_not_callable_object(type_name))\n            } else {\n                // Attribute doesn't exist — use the class name (e.g., \"Point\") not \"Dataclass\"\n                Err(ExcType::attribute_error(self.name(interns), method_name))\n            }\n        }\n    }\n\n    fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<CallResult>> {\n        let attr_name = attr.as_str(vm.interns);\n        match self.attrs.get_by_str(attr_name, vm.heap, vm.interns) {\n            Some(value) => Ok(Some(CallResult::Value(value.clone_with_heap(vm.heap)))),\n            // we use name here, not `self.py_type(heap)` hence returning a Ok(None)\n            None => Err(ExcType::attribute_error(self.name(vm.interns), attr_name)),\n        }\n    }\n}\n\nimpl HeapItem for Dataclass {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n            + self.name.py_estimate_size()\n            + self.field_names.iter().map(String::len).sum::<usize>()\n            + self.attrs.py_estimate_size()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Delegate to the attrs Dict which handles all nested heap references\n        self.attrs.py_dec_ref_ids(stack);\n    }\n}\n\n// Custom serde implementation for Dataclass.\n// Serializes all five fields.\nimpl serde::Serialize for Dataclass {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        use serde::ser::SerializeStruct;\n        let mut state = serializer.serialize_struct(\"Dataclass\", 5)?;\n        state.serialize_field(\"name\", &self.name)?;\n        state.serialize_field(\"type_id\", &self.type_id)?;\n        state.serialize_field(\"field_names\", &self.field_names)?;\n        state.serialize_field(\"attrs\", &self.attrs)?;\n        state.serialize_field(\"frozen\", &self.frozen)?;\n        state.end()\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for Dataclass {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        #[derive(serde::Deserialize)]\n        struct DataclassData {\n            name: EitherStr,\n            type_id: u64,\n            field_names: Vec<String>,\n            attrs: Dict,\n            frozen: bool,\n        }\n        let dc = DataclassData::deserialize(deserializer)?;\n        Ok(Self {\n            name: dc.name,\n            type_id: dc.type_id,\n            field_names: dc.field_names,\n            attrs: dc.attrs,\n            frozen: dc.frozen,\n        })\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/dict.rs",
    "content": "use std::{\n    collections::hash_map::DefaultHasher,\n    fmt::Write,\n    hash::{Hash, Hasher},\n};\n\nuse ahash::AHashSet;\nuse hashbrown::{HashTable, hash_table::Entry};\nuse smallvec::smallvec;\n\nuse super::{DictItemsView, DictKeysView, DictValuesView, MontyIter, PyTrait, allocate_tuple};\nuse crate::{\n    args::{ArgValues, KwargsValues},\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult},\n    heap::{ContainsHeap, DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem},\n    intern::{Interns, StaticStrings},\n    resource::{ResourceError, ResourceTracker},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Python dict type preserving insertion order.\n///\n/// This type provides Python dict semantics including dynamic key-value namespaces,\n/// reference counting for heap values, and standard dict methods.\n///\n/// # Implemented Methods\n/// - `get(key[, default])` - Get value or default\n/// - `keys()` - Return view of keys\n/// - `values()` - Return view of values\n/// - `items()` - Return view of (key, value) pairs\n/// - `pop(key[, default])` - Remove and return value\n/// - `clear()` - Remove all items\n/// - `copy()` - Shallow copy\n/// - `update(other)` - Update from dict or iterable of pairs\n/// - `setdefault(key[, default])` - Get or set default value\n/// - `popitem()` - Remove and return last (key, value) pair\n/// - `fromkeys(iterable[, value])` - Create dict from keys (classmethod)\n///\n/// All dict methods from Python's builtins are implemented.\n///\n/// # Storage Strategy\n/// Uses a `HashTable<usize>` for hash lookups combined with a dense `Vec<DictEntry>`\n/// to preserve insertion order (matching Python 3.7+ behavior). The hash table maps\n/// key hashes to indices in the entries vector. This design provides O(1) lookups\n/// while maintaining insertion order for iteration.\n///\n/// # Reference Counting\n/// When values are added via `set()`, their reference counts are incremented.\n/// When using `from_pairs()`, ownership is transferred without incrementing refcounts\n/// (caller must ensure values' refcounts account for the dict's reference).\n///\n/// # GC Optimization\n/// The `contains_refs` flag tracks whether the dict contains any `Value::Ref` items.\n/// This allows `collect_child_ids` and `py_dec_ref_ids` to skip iteration when the\n/// dict contains only primitive values (ints, bools, None, etc.), significantly\n/// improving GC performance for dicts of primitives.\n#[derive(Debug, Default)]\npub(crate) struct Dict {\n    /// indices mapping from the entry hash to its index.\n    indices: HashTable<usize>,\n    /// entries is a dense vec maintaining entry order.\n    entries: Vec<DictEntry>,\n    /// True if any key or value in the dict is a `Value::Ref`. Used to skip iteration\n    /// in `collect_child_ids` and `py_dec_ref_ids` when no refs are present.\n    /// Only transitions from false to true (never back) since tracking removals would be O(n).\n    contains_refs: bool,\n}\n\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nstruct DictEntry {\n    key: Value,\n    value: Value,\n    /// the hash is needed here for correct use of insert_unique\n    hash: u64,\n}\n\nimpl Dict {\n    /// Creates a new empty dict.\n    #[must_use]\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn with_capacity(capacity: usize) -> Self {\n        Self {\n            indices: HashTable::with_capacity(capacity),\n            entries: Vec::with_capacity(capacity),\n            contains_refs: false,\n        }\n    }\n\n    /// Returns whether this dict contains any heap references (`Value::Ref`).\n    ///\n    /// Used during allocation to determine if this container could create cycles,\n    /// and in `collect_child_ids` and `py_dec_ref_ids` to skip iteration when no refs\n    /// are present.\n    ///\n    /// Note: This flag only transitions from false to true (never back). When a ref is\n    /// removed via `pop()`, we do NOT recompute the flag because that would be O(n).\n    /// This is conservative - we may iterate unnecessarily if all refs were removed,\n    /// but we'll never skip iteration when refs exist.\n    #[inline]\n    #[must_use]\n    pub fn has_refs(&self) -> bool {\n        self.contains_refs\n    }\n\n    /// Creates a dict from a vector of (key, value) pairs.\n    ///\n    /// Assumes the caller is transferring ownership of all keys and values in the pairs.\n    /// Does NOT increment reference counts since ownership is being transferred.\n    /// Returns Err if any key is unhashable (e.g., list, dict).\n    pub fn from_pairs(pairs: Vec<(Value, Value)>, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let pairs_iter = pairs.into_iter();\n        defer_drop_mut!(pairs_iter, vm);\n        let dict = Self::with_capacity(pairs_iter.len());\n        let mut dict_guard = HeapGuard::new(dict, vm);\n        let (dict, vm) = dict_guard.as_parts_mut();\n        for (key, value) in pairs_iter {\n            if let Some(old_value) = dict.set(key, value, vm)? {\n                old_value.drop_with_heap(vm);\n            }\n        }\n        Ok(dict_guard.into_inner())\n    }\n\n    /// Gets a value from the dict by key.\n    ///\n    /// Returns Ok(Some(value)) if key exists, Ok(None) if key doesn't exist.\n    /// Returns Err if key is unhashable.\n    pub fn get(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<&Value>> {\n        if let Some(index) = self.find_index_hash(key, vm)?.0 {\n            Ok(Some(&self.entries[index].value))\n        } else {\n            Ok(None)\n        }\n    }\n\n    /// Gets a value from the dict by string key name (immutable lookup).\n    ///\n    /// This is an O(1) lookup that doesn't require mutable heap access.\n    /// Only works for string keys - returns None if the key is not found.\n    pub fn get_by_str(&self, key_str: &str, heap: &Heap<impl ResourceTracker>, interns: &Interns) -> Option<&Value> {\n        // Compute hash for the string key\n        let mut hasher = DefaultHasher::new();\n        key_str.hash(&mut hasher);\n        let hash = hasher.finish();\n\n        // Find entry with matching hash and key\n        self.indices\n            .find(hash, |&idx| {\n                let entry_key = &self.entries[idx].key;\n                match entry_key {\n                    Value::InternString(id) => interns.get_str(*id) == key_str,\n                    Value::Ref(id) => {\n                        if let HeapData::Str(s) = heap.get(*id) {\n                            s.as_str() == key_str\n                        } else {\n                            false\n                        }\n                    }\n                    _ => false,\n                }\n            })\n            .map(|&idx| &self.entries[idx].value)\n    }\n\n    /// Sets a key-value pair in the dict.\n    ///\n    /// The caller transfers ownership of `key` and `value` to the dict. Their refcounts\n    /// are NOT incremented here - the caller is responsible for ensuring the refcounts\n    /// were already incremented (e.g., via `clone_with_heap` or `evaluate_use`).\n    ///\n    /// If the key already exists, replaces the old value and returns it (caller now\n    /// owns the old value and is responsible for its refcount).\n    /// Returns Err if key is unhashable.\n    pub fn set(\n        &mut self,\n        key: Value,\n        value: Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Option<Value>> {\n        // Track if we're adding a reference for GC optimization\n        if matches!(key, Value::Ref(_)) || matches!(value, Value::Ref(_)) {\n            self.contains_refs = true;\n        }\n\n        // Handle hash computation errors explicitly so we can drop key/value properly\n        let (opt_index, hash) = match self.find_index_hash(&key, vm) {\n            Ok(result) => result,\n            Err(e) => {\n                // Drop the key and value before returning the error\n                key.drop_with_heap(vm);\n                value.drop_with_heap(vm);\n                return Err(e);\n            }\n        };\n\n        let entry = DictEntry { key, value, hash };\n        if let Some(index) = opt_index {\n            // Key exists, replace in place to preserve insertion order\n            let old_entry = std::mem::replace(&mut self.entries[index], entry);\n\n            // Decrement refcount for old key (we're discarding it)\n            old_entry.key.drop_with_heap(vm);\n            // Transfer ownership of the old value to caller (no clone needed)\n            Ok(Some(old_entry.value))\n        } else {\n            // Key doesn't exist, add new pair to indices and entries\n            let index = self.entries.len();\n            self.entries.push(entry);\n            self.indices\n                .insert_unique(hash, index, |index| self.entries[*index].hash);\n            Ok(None)\n        }\n    }\n\n    /// Removes and returns a key-value pair from the dict.\n    ///\n    /// Returns Ok(Some((key, value))) if key exists, Ok(None) if key doesn't exist.\n    /// Returns Err if key is unhashable.\n    ///\n    /// Reference counting: does not decrement refcounts for removed key and value;\n    /// caller assumes ownership and is responsible for managing their refcounts.\n    pub fn pop(&mut self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<(Value, Value)>> {\n        let hash = key\n            .py_hash(vm.heap, vm.interns)?\n            .ok_or_else(|| ExcType::type_error_unhashable_dict_key(key.py_type(vm.heap)))?;\n\n        let entry = self.indices.entry(\n            hash,\n            |v| key.py_eq(&self.entries[*v].key, vm).unwrap_or(false),\n            |index| self.entries[*index].hash,\n        );\n\n        if let Entry::Occupied(occ_entry) = entry {\n            let entry = self.entries.remove(*occ_entry.get());\n            occ_entry.remove();\n            // Don't decrement refcounts - caller now owns the values\n            Ok(Some((entry.key, entry.value)))\n        } else {\n            Ok(None)\n        }\n    }\n\n    /// Returns the number of key-value pairs in the dict.\n    #[must_use]\n    pub fn len(&self) -> usize {\n        self.entries.len()\n    }\n\n    /// Returns true if the dict is empty.\n    #[must_use]\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    /// Returns an iterator over references to (key, value) pairs.\n    pub fn iter(&self) -> DictIter<'_> {\n        self.into_iter()\n    }\n\n    /// Returns the key at the given iteration index, or None if out of bounds.\n    ///\n    /// Used for index-based iteration in for loops. Returns a reference to\n    /// the key at the given position in insertion order.\n    pub fn key_at(&self, index: usize) -> Option<&Value> {\n        self.entries.get(index).map(|e| &e.key)\n    }\n\n    /// Returns the value at the given iteration index, or None if out of bounds.\n    ///\n    /// Dictionary views use this to produce live `dict_values` iteration directly\n    /// from the underlying storage without copying the dictionary.\n    pub fn value_at(&self, index: usize) -> Option<&Value> {\n        self.entries.get(index).map(|e| &e.value)\n    }\n\n    /// Returns the key-value pair at the given iteration index, or None if out of bounds.\n    ///\n    /// This accessor keeps dict-view iteration logic out of the storage internals\n    /// while still allowing `dict_items` to produce tuples on demand.\n    pub fn item_at(&self, index: usize) -> Option<(&Value, &Value)> {\n        self.entries.get(index).map(|entry| (&entry.key, &entry.value))\n    }\n\n    /// Creates a dict from the `dict([mapping_or_pairs], **kwargs)` constructor call.\n    ///\n    /// Supported forms:\n    /// - `dict()` returns an empty dict.\n    /// - `dict(existing_dict)` returns a shallow copy of the dict.\n    /// - `dict(iterable_of_pairs)` consumes `(key, value)` pairs from the iterable.\n    /// - `dict(**kwargs)` inserts keyword arguments as string keys.\n    ///\n    /// Keyword arguments are applied after the optional positional source, matching\n    /// CPython precedence (`dict([('a', 1)], a=2)` yields `{'a': 2}`).\n    ///\n    /// For now, only real `dict` values use mapping-copy semantics; other values\n    /// are interpreted as iterables of pairs.\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let dict = Self::new();\n        let mut dict_guard = HeapGuard::new(dict, vm);\n\n        {\n            let (dict, vm) = dict_guard.as_parts_mut();\n            let (pos_iter, kwargs) = args.into_parts();\n            defer_drop_mut!(pos_iter, vm);\n            let mut kwargs_guard = HeapGuard::new(kwargs, vm);\n\n            if let Some(other_value) = pos_iter.next() {\n                let other_value_guard = HeapGuard::new(other_value, kwargs_guard.heap());\n                if pos_iter.len() != 0 {\n                    return Err(ExcType::type_error_at_most(\"dict\", 1, pos_iter.len() + 1));\n                }\n                let other_value = other_value_guard.into_inner();\n                dict_merge_from_value(dict, other_value, kwargs_guard.heap())?;\n            }\n\n            let kwargs = kwargs_guard.into_inner();\n            dict_merge_from_kwargs(dict, kwargs, vm)?;\n        }\n\n        let dict = dict_guard.into_inner();\n        let heap_id = vm.heap.allocate(HeapData::Dict(dict))?;\n        Ok(Value::Ref(heap_id))\n    }\n\n    fn find_index_hash(\n        &self,\n        key: &Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<(Option<usize>, u64)> {\n        let hash = key\n            .py_hash(vm.heap, vm.interns)?\n            .ok_or_else(|| ExcType::type_error_unhashable_dict_key(key.py_type(vm.heap)))?;\n\n        // Dict keys are typically shallow (strings, ints, tuples of primitives),\n        // so recursion errors are unlikely. If one occurs, treat it as \"not equal\" -\n        // the key lookup fails but doesn't crash.\n        let opt_index = self\n            .indices\n            .find(hash, |v| key.py_eq(&self.entries[*v].key, vm).unwrap_or(false))\n            .copied();\n        Ok((opt_index, hash))\n    }\n}\n\n/// Iterator over borrowed (key, value) pairs in a dict.\npub(crate) struct DictIter<'a>(std::slice::Iter<'a, DictEntry>);\n\nimpl<'a> Iterator for DictIter<'a> {\n    type Item = (&'a Value, &'a Value);\n    fn next(&mut self) -> Option<Self::Item> {\n        self.0.next().map(|e| (&e.key, &e.value))\n    }\n}\n\nimpl<'a> IntoIterator for &'a Dict {\n    type Item = (&'a Value, &'a Value);\n    type IntoIter = DictIter<'a>;\n    fn into_iter(self) -> Self::IntoIter {\n        DictIter(self.entries.iter())\n    }\n}\n\n/// Iterator over owned (key, value) pairs from a consumed dict.\npub(crate) struct DictIntoIter(std::vec::IntoIter<DictEntry>);\n\nimpl Iterator for DictIntoIter {\n    type Item = (Value, Value);\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.0.next().map(|e| (e.key, e.value))\n    }\n\n    fn size_hint(&self) -> (usize, Option<usize>) {\n        self.0.size_hint()\n    }\n}\n\nimpl ExactSizeIterator for DictIntoIter {}\n\nimpl IntoIterator for Dict {\n    type Item = (Value, Value);\n    type IntoIter = DictIntoIter;\n    fn into_iter(self) -> Self::IntoIter {\n        DictIntoIter(self.entries.into_iter())\n    }\n}\n\nimpl PyTrait for Dict {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Dict\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.len())\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.len() != other.len() {\n            return Ok(false);\n        }\n\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        for entry in &self.entries {\n            vm.heap.check_time()?;\n            if let Ok(Some(other_v)) = other.get(&entry.key, vm) {\n                if !entry.value.py_eq(other_v, vm)? {\n                    return Ok(false);\n                }\n            } else {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        if self.is_empty() {\n            return f.write_str(\"{}\");\n        }\n\n        let heap = &*vm.heap;\n        // Check depth limit before recursing\n        let Some(token) = heap.incr_recursion_depth_for_repr() else {\n            return f.write_str(\"{...}\");\n        };\n        crate::defer_drop_immutable_heap!(token, heap);\n\n        f.write_char('{')?;\n        let mut first = true;\n        for entry in &self.entries {\n            if !first {\n                if heap.check_time().is_err() {\n                    f.write_str(\", ...[timeout]\")?;\n                    break;\n                }\n                f.write_str(\", \")?;\n            }\n            first = false;\n            entry.key.py_repr_fmt(f, vm, heap_ids)?;\n            f.write_str(\": \")?;\n            entry.value.py_repr_fmt(f, vm, heap_ids)?;\n        }\n        f.write_char('}')?;\n\n        Ok(())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        match self.get(key, vm)? {\n            Some(value) => Ok(value.clone_with_heap(vm)),\n            None => Err(ExcType::key_error(key, vm)),\n        }\n    }\n\n    fn py_setitem(&mut self, key: Value, value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        // Drop the old value if one was replaced\n        if let Some(old_value) = self.set(key, value, vm)? {\n            old_value.drop_with_heap(vm);\n        }\n        Ok(())\n    }\n\n    fn py_call_attr(\n        &mut self,\n        self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let Some(method) = attr.static_string() else {\n            args.drop_with_heap(vm.heap);\n            return Err(ExcType::attribute_error(Type::Dict, attr.as_str(vm.interns)));\n        };\n\n        let value = match method {\n            StaticStrings::Get => {\n                // dict.get() accepts 1 or 2 arguments\n                let (key, default) = args.get_one_two_args(\"get\", vm.heap)?;\n                defer_drop!(key, vm);\n                let default = default.unwrap_or(Value::None);\n                let mut default_guard = HeapGuard::new(default, vm);\n                let vm = default_guard.heap();\n                // Handle the lookup - may fail for unhashable keys\n                let value = match self.get(key, vm)? {\n                    Some(v) => v.clone_with_heap(vm),\n                    None => default_guard.into_inner(),\n                };\n                Ok(value)\n            }\n            StaticStrings::Keys => {\n                args.check_zero_args(\"dict.keys\", vm.heap)?;\n                let view_id = vm.heap.allocate(HeapData::DictKeysView(DictKeysView::new(self_id)))?;\n                vm.heap.inc_ref(self_id);\n                Ok(Value::Ref(view_id))\n            }\n            StaticStrings::Values => {\n                args.check_zero_args(\"dict.values\", vm.heap)?;\n                let view_id = vm\n                    .heap\n                    .allocate(HeapData::DictValuesView(DictValuesView::new(self_id)))?;\n                vm.heap.inc_ref(self_id);\n                Ok(Value::Ref(view_id))\n            }\n            StaticStrings::Items => {\n                args.check_zero_args(\"dict.items\", vm.heap)?;\n                let view_id = vm.heap.allocate(HeapData::DictItemsView(DictItemsView::new(self_id)))?;\n                vm.heap.inc_ref(self_id);\n                Ok(Value::Ref(view_id))\n            }\n            StaticStrings::Pop => {\n                // dict.pop() accepts 1 or 2 arguments (key, optional default)\n                let (key, default) = args.get_one_two_args(\"pop\", vm.heap)?;\n                defer_drop!(key, vm);\n                let mut default_guard = HeapGuard::new(default, vm);\n                let vm = default_guard.heap();\n                if let Some((old_key, value)) = self.pop(key, vm)? {\n                    // Drop the old key - we don't need it\n                    old_key.drop_with_heap(vm);\n                    Ok(value)\n                } else {\n                    let (default, vm) = default_guard.into_parts();\n                    // No matching key - return default if provided, else KeyError\n                    if let Some(d) = default {\n                        Ok(d)\n                    } else {\n                        Err(ExcType::key_error(key, vm))\n                    }\n                }\n            }\n            StaticStrings::Clear => {\n                args.check_zero_args(\"dict.clear\", vm.heap)?;\n                dict_clear(self, vm.heap);\n                Ok(Value::None)\n            }\n            StaticStrings::Copy => {\n                args.check_zero_args(\"dict.copy\", vm.heap)?;\n                dict_copy(self, vm)\n            }\n            StaticStrings::Update => dict_update(self, args, vm),\n            StaticStrings::Setdefault => dict_setdefault(self, args, vm),\n            StaticStrings::Popitem => {\n                args.check_zero_args(\"dict.popitem\", vm.heap)?;\n                dict_popitem(self, vm.heap)\n            }\n            // fromkeys is a classmethod but also accessible on instances\n            StaticStrings::Fromkeys => dict_fromkeys(args, vm),\n            _ => {\n                args.drop_with_heap(vm.heap);\n                return Err(ExcType::attribute_error(Type::Dict, attr.as_str(vm.interns)));\n            }\n        };\n        value.map(CallResult::Value)\n    }\n}\n\nimpl HeapItem for Dict {\n    fn py_estimate_size(&self) -> usize {\n        // Dict size: struct overhead + entries (2 Values per entry for key+value)\n        std::mem::size_of::<Self>() + self.len() * 2 * std::mem::size_of::<Value>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Skip iteration if no refs - major GC optimization for dicts of primitives\n        if !self.contains_refs {\n            return;\n        }\n        for entry in &mut self.entries {\n            if let Value::Ref(id) = &entry.key {\n                stack.push(*id);\n                #[cfg(feature = \"ref-count-panic\")]\n                entry.key.dec_ref_forget();\n            }\n            if let Value::Ref(id) = &entry.value {\n                stack.push(*id);\n                #[cfg(feature = \"ref-count-panic\")]\n                entry.value.dec_ref_forget();\n            }\n        }\n    }\n}\n\nimpl DropWithHeap for Dict {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        self.entries.drop_with_heap(heap);\n    }\n}\n\nimpl DropWithHeap for DictEntry {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        self.key.drop_with_heap(heap);\n        self.value.drop_with_heap(heap);\n    }\n}\n\n/// Implements Python's `dict.clear()` method.\n///\n/// Removes all items from the dict.\nfn dict_clear(dict: &mut Dict, heap: &mut Heap<impl ResourceTracker>) {\n    dict.entries.drain(..).drop_with_heap(heap);\n    dict.indices.clear();\n    // Note: contains_refs stays true even if all refs removed, per conservative GC strategy\n}\n\n/// Implements Python's `dict.copy()` method.\n///\n/// Returns a shallow copy of the dict.\nfn dict_copy(dict: &Dict, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    // Copy all key-value pairs (incrementing refcounts)\n    let pairs: Vec<(Value, Value)> = dict\n        .iter()\n        .map(|(k, v)| (k.clone_with_heap(vm), v.clone_with_heap(vm)))\n        .collect();\n\n    let new_dict = Dict::from_pairs(pairs, vm)?;\n    let heap_id = vm.heap.allocate(HeapData::Dict(new_dict))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Implements Python's `dict.update([other], **kwargs)` method.\n///\n/// Updates the dict with key-value pairs from `other` and/or `kwargs`.\n/// If `other` is a dict, copies its key-value pairs.\n/// If `other` is an iterable, expects pairs of (key, value).\n/// Keyword arguments are also added to the dict.\nfn dict_update(dict: &mut Dict, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (pos_iter, kwargs) = args.into_parts();\n    defer_drop_mut!(pos_iter, vm);\n    let mut kwargs_guard = HeapGuard::new(kwargs, vm);\n\n    if let Some(other_value) = pos_iter.next() {\n        let other_value_guard = HeapGuard::new(other_value, kwargs_guard.heap());\n        if pos_iter.len() != 0 {\n            return Err(ExcType::type_error_at_most(\"dict.update\", 1, pos_iter.len() + 1));\n        }\n        let other_value = other_value_guard.into_inner();\n        dict_merge_from_value(dict, other_value, kwargs_guard.heap())?;\n    }\n\n    let kwargs = kwargs_guard.into_inner();\n    dict_merge_from_kwargs(dict, kwargs, vm)?;\n    Ok(Value::None)\n}\n\n/// Merges key-value pairs from either a dict or an iterable of 2-item pairs.\n///\n/// This is shared between `dict()` construction and `dict.update()` so both\n/// entry points follow identical positional-source semantics.\nfn dict_merge_from_value(\n    dict: &mut Dict,\n    other_value: Value,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<()> {\n    let mut other_value_guard = HeapGuard::new(other_value, vm);\n    {\n        let (other_value, vm) = other_value_guard.as_parts();\n        if let Value::Ref(id) = other_value\n            && let HeapData::Dict(src_dict) = vm.heap.get(*id)\n        {\n            // Clone key-value pairs from the source dict.\n            let pairs: Vec<(Value, Value)> = src_dict\n                .iter()\n                .map(|(k, v)| (k.clone_with_heap(vm.heap), v.clone_with_heap(vm.heap)))\n                .collect();\n\n            // Apply pairs into the target dict.\n            for (key, value) in pairs {\n                let old_value = dict.set(key, value, vm)?;\n                old_value.drop_with_heap(vm.heap);\n            }\n            return Ok(());\n        }\n    }\n\n    // Non-dict values are interpreted as iterable-of-pairs.\n    let other_value = other_value_guard.into_inner();\n    dict_merge_from_iterable_pairs(dict, other_value, vm)\n}\n\n/// Merges key-value pairs from an iterable of 2-item iterables.\n///\n/// Each item from `iterable` is treated as `(key, value)`. Items with length 0, 1,\n/// or greater than 2 raise the same TypeError messages used by `dict.update()`.\nfn dict_merge_from_iterable_pairs(\n    dict: &mut Dict,\n    iterable: Value,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<()> {\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n\n    while let Some(item) = iter.for_next(vm)? {\n        // Each item should be a pair (iterable of 2 elements).\n        let pair_iter = MontyIter::new(item, vm)?;\n        defer_drop_mut!(pair_iter, vm);\n\n        let Some(key) = pair_iter.for_next(vm)? else {\n            return Err(ExcType::type_error(\n                \"dictionary update sequence element has length 0; 2 is required\",\n            ));\n        };\n        let mut key_guard = HeapGuard::new(key, vm);\n\n        let Some(value) = pair_iter.for_next(key_guard.heap())? else {\n            return Err(ExcType::type_error(\n                \"dictionary update sequence element has length 1; 2 is required\",\n            ));\n        };\n        let mut value_guard = HeapGuard::new(value, key_guard.heap());\n\n        if let Some(extra) = pair_iter.for_next(value_guard.heap())? {\n            extra.drop_with_heap(value_guard.heap());\n            return Err(ExcType::type_error(\n                \"dictionary update sequence element has length > 2; 2 is required\",\n            ));\n        }\n\n        let value = value_guard.into_inner();\n        let key = key_guard.into_inner();\n\n        if let Some(old_value) = dict.set(key, value, vm)? {\n            old_value.drop_with_heap(vm);\n        }\n    }\n\n    Ok(())\n}\n\n/// Merges keyword arguments into a dict.\n///\n/// This helper drains `kwargs` safely on error so all values are dropped\n/// correctly, then inserts each key-value pair into `dict`.\nfn dict_merge_from_kwargs(\n    dict: &mut Dict,\n    kwargs: KwargsValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<()> {\n    let kwargs_iter = kwargs.into_iter();\n    defer_drop_mut!(kwargs_iter, vm);\n    for (key, value) in kwargs_iter {\n        let old_value = dict.set(key, value, vm)?;\n        old_value.drop_with_heap(vm);\n    }\n    Ok(())\n}\n\n/// Implements Python's `dict.setdefault(key[, default])` method.\n///\n/// If key is in the dict, return its value.\n/// If not, insert key with a value of default (or None) and return default.\nfn dict_setdefault(dict: &mut Dict, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (key, default) = args.get_one_two_args(\"setdefault\", vm.heap)?;\n    let default = default.unwrap_or(Value::None);\n    let mut key_guard = HeapGuard::new(key, vm);\n    let (key, vm) = key_guard.as_parts();\n\n    if let Some(existing) = dict.get(key, vm)? {\n        // Key exists - return its value (cloned)\n        let value = existing.clone_with_heap(vm);\n        default.drop_with_heap(vm);\n        Ok(value)\n    } else {\n        // Key doesn't exist - insert default and return it (cloned before insertion)\n        let return_value = default.clone_with_heap(vm);\n        let (key, vm) = key_guard.into_parts();\n        if let Some(old_value) = dict.set(key, default, vm)? {\n            // This shouldn't happen since we checked, but handle it anyway\n            old_value.drop_with_heap(vm);\n        }\n        Ok(return_value)\n    }\n}\n\n/// Implements Python's `dict.popitem()` method.\n///\n/// Removes and returns the last inserted key-value pair as a tuple.\n/// Raises KeyError if the dict is empty.\nfn dict_popitem(dict: &mut Dict, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    if dict.is_empty() {\n        return Err(ExcType::key_error_popitem_empty_dict());\n    }\n\n    // Remove the last entry (LIFO order)\n    let entry = dict.entries.pop().expect(\"dict is not empty\");\n\n    // Remove from indices - need to find the entry with this index\n    // Since we removed the last entry, we need to clear and rebuild indices\n    // (This is simpler than trying to find and remove the specific hash entry)\n    // TODO: This O(n) rebuild could be optimized by finding and removing the\n    // specific hash entry directly from the hashbrown table.\n    dict.indices.clear();\n    for (idx, e) in dict.entries.iter().enumerate() {\n        dict.indices.insert_unique(e.hash, idx, |&i| dict.entries[i].hash);\n    }\n\n    // Create tuple (key, value)\n    Ok(allocate_tuple(smallvec![entry.key, entry.value], heap)?)\n}\n\n// Custom serde implementation for Dict.\n// Serializes entries and contains_refs; rebuilds the indices hash table on deserialize.\nimpl serde::Serialize for Dict {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        use serde::ser::SerializeStruct;\n        let mut state = serializer.serialize_struct(\"Dict\", 2)?;\n        state.serialize_field(\"entries\", &self.entries)?;\n        state.serialize_field(\"contains_refs\", &self.contains_refs)?;\n        state.end()\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for Dict {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        #[derive(serde::Deserialize)]\n        struct DictFields {\n            entries: Vec<DictEntry>,\n            contains_refs: bool,\n        }\n        let fields = DictFields::deserialize(deserializer)?;\n        // Rebuild the indices hash table from the entries\n        let mut indices = HashTable::with_capacity(fields.entries.len());\n        for (idx, entry) in fields.entries.iter().enumerate() {\n            indices.insert_unique(entry.hash, idx, |&i| fields.entries[i].hash);\n        }\n        Ok(Self {\n            indices,\n            entries: fields.entries,\n            contains_refs: fields.contains_refs,\n        })\n    }\n}\n\n/// Implements Python's `dict.fromkeys(iterable[, value])` classmethod.\n///\n/// Creates a new dictionary with keys from `iterable` and all values set to `value`\n/// (default: None).\n///\n/// This is a classmethod that can be called directly on the dict type:\n/// ```python\n/// dict.fromkeys(['a', 'b', 'c'])  # {'a': None, 'b': None, 'c': None}\n/// dict.fromkeys(['a', 'b'], 0)    # {'a': 0, 'b': 0}\n/// ```\npub fn dict_fromkeys(args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (iterable, default) = args.get_one_two_args(\"dict.fromkeys\", vm.heap)?;\n    let default = default.unwrap_or(Value::None);\n    defer_drop!(default, vm);\n\n    let iter = MontyIter::new(iterable, vm)?;\n    defer_drop_mut!(iter, vm);\n\n    let dict = Dict::new();\n    let mut dict_guard = HeapGuard::new(dict, vm);\n\n    {\n        let (dict, vm) = dict_guard.as_parts_mut();\n\n        while let Some(key) = iter.for_next(vm)? {\n            let old_value = dict.set(key, default.clone_with_heap(vm), vm)?;\n            old_value.drop_with_heap(vm);\n        }\n    }\n\n    let dict = dict_guard.into_inner();\n    let heap_id = vm.heap.allocate(HeapData::Dict(dict))?;\n    Ok(Value::Ref(heap_id))\n}\n"
  },
  {
    "path": "crates/monty/src/types/dict_view.rs",
    "content": "use std::fmt::Write;\n\nuse ahash::AHashSet;\nuse smallvec::smallvec;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunError, RunResult},\n    heap::{Heap, HeapData, HeapId, HeapItem},\n    heap_data::HeapDataMut,\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    types::{Dict, FrozenSet, MontyIter, PyTrait, Set, Type, allocate_tuple, iter::advance_on_heap},\n    value::Value,\n};\n\n/// Shared accessors for heap-backed dictionary view objects.\n///\n/// All dictionary views are thin live references to an underlying `dict`. They do\n/// not snapshot keys, items, or values; instead every observable operation reads\n/// through to the current dict state. Keeping that behavior centralized avoids\n/// subtle divergence between keys/items/values views.\npub(crate) trait DictView {\n    /// Returns the heap id of the underlying dictionary this view keeps alive.\n    fn dict_id(&self) -> HeapId;\n\n    /// Returns the live dictionary backing this view.\n    fn dict<'a>(&self, heap: &'a Heap<impl ResourceTracker>) -> &'a Dict {\n        let HeapData::Dict(dict) = heap.get(self.dict_id()) else {\n            panic!(\"dict view must always reference a dict\");\n        };\n        dict\n    }\n}\n\n/// Live view returned by `dict.keys()`.\n///\n/// `dict_keys` is set-like in CPython, so this view supports the shared live-view\n/// behavior plus equality against other keys views and ordinary set-like values.\n/// The remaining set algebra operations are added incrementally in the VM layer.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub(crate) struct DictKeysView {\n    dict_id: HeapId,\n}\n\nimpl DictKeysView {\n    /// Creates a new keys view over an existing dictionary heap entry.\n    #[must_use]\n    pub fn new(dict_id: HeapId) -> Self {\n        Self { dict_id }\n    }\n\n    /// Returns the underlying dictionary heap id.\n    #[must_use]\n    pub fn dict_id(self) -> HeapId {\n        self.dict_id\n    }\n\n    /// Compares this keys view to another keys view using set semantics.\n    ///\n    /// Two keys views compare equal when they expose the same live key set,\n    /// even if they are distinct view objects over distinct dictionaries.\n    pub(crate) fn eq_view(self, other: Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.dict_id == other.dict_id {\n            return Ok(true);\n        }\n\n        Heap::with_two(vm, self.dict_id, other.dict_id, |vm, left, right| {\n            let (HeapData::Dict(left_dict), HeapData::Dict(right_dict)) = (left, right) else {\n                panic!(\"dict_keys view must always reference dicts\");\n            };\n            dict_keys_eq_dict(left_dict, right_dict, vm)\n        })\n    }\n\n    /// Compares this keys view to a mutable set using set membership semantics.\n    pub(crate) fn eq_set(self, other: &Set, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Heap::with_entry_mut(vm, self.dict_id, |vm, data| {\n            let HeapDataMut::Dict(dict) = data else {\n                panic!(\"dict_keys view must always reference a dict\");\n            };\n            dict_keys_eq_set_like(\n                dict,\n                other.len(),\n                |key, vm| matches!(other.contains(key, vm), Ok(true)),\n                vm,\n            )\n        })\n    }\n\n    /// Compares this keys view to a frozenset using set membership semantics.\n    pub(crate) fn eq_frozenset(\n        self,\n        other: &FrozenSet,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<bool, ResourceError> {\n        Heap::with_entry_mut(vm, self.dict_id, |vm, data| {\n            let HeapDataMut::Dict(dict) = data else {\n                panic!(\"dict_keys view must always reference a dict\");\n            };\n            dict_keys_eq_set_like(\n                dict,\n                other.len(),\n                |key, vm| matches!(other.contains(key, vm), Ok(true)),\n                vm,\n            )\n        })\n    }\n\n    /// Materializes the view's current live keys into a plain `set`.\n    ///\n    /// Dict-view operators always produce ordinary `set` results in CPython,\n    /// so the VM uses this helper as the left-hand-side snapshot for `& | ^ -`\n    /// and for `isdisjoint(...)`.\n    pub(crate) fn to_set(self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Set> {\n        Heap::with_entry_mut(vm, self.dict_id, |vm, data| {\n            let HeapDataMut::Dict(dict) = data else {\n                panic!(\"dict_keys view must always reference a dict\");\n            };\n\n            let mut result = Set::with_capacity(dict.len());\n            for (key, _) in dict.iter() {\n                result.add(key.clone_with_heap(vm), vm)?;\n            }\n            Ok(result)\n        })\n    }\n\n    /// Implements `dict_keys.isdisjoint(iterable)` with CPython's iterable semantics.\n    pub(crate) fn isdisjoint_from_value(\n        self,\n        other: &Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<bool> {\n        let self_set = self.to_set(vm)?;\n        defer_drop!(self_set, vm);\n        let other_set = collect_iterable_to_set(other.clone_with_heap(vm), vm)?;\n        defer_drop!(other_set, vm);\n        sets_are_disjoint(self_set, other_set, vm)\n    }\n}\n\nimpl DictView for DictKeysView {\n    fn dict_id(&self) -> HeapId {\n        self.dict_id\n    }\n}\n\nimpl PyTrait for DictKeysView {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::DictKeys\n    }\n\n    fn py_len(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.dict(vm.heap).len())\n    }\n\n    fn py_eq(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<bool, crate::resource::ResourceError> {\n        self.eq_view(*other, vm)\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        f.write_str(\"dict_keys([\")?;\n        write_dict_keys_contents(f, self.dict(vm.heap), vm, heap_ids)?;\n        f.write_str(\"])\")\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &crate::value::EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        match attr.static_string() {\n            Some(StaticStrings::Isdisjoint) => {\n                let other = args.get_one_arg(\"dict_keys.isdisjoint\", vm.heap)?;\n                defer_drop!(other, vm);\n                Ok(CallResult::Value(Value::Bool(self.isdisjoint_from_value(other, vm)?)))\n            }\n            _ => Err(ExcType::attribute_error(Type::DictKeys, attr.as_str(vm.interns))),\n        }\n    }\n}\n\nimpl HeapItem for DictKeysView {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        stack.push(self.dict_id);\n    }\n}\n\n/// Live view returned by `dict.items()`.\n///\n/// The view stays linked to the original dictionary so iteration, `len()`, and\n/// repr all reflect subsequent dictionary mutations. Like CPython, equality is\n/// set-like: items views compare by their live `(key, value)` pairs.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub(crate) struct DictItemsView {\n    dict_id: HeapId,\n}\n\nimpl DictItemsView {\n    /// Creates a new items view over an existing dictionary heap entry.\n    #[must_use]\n    pub fn new(dict_id: HeapId) -> Self {\n        Self { dict_id }\n    }\n\n    /// Returns the underlying dictionary heap id.\n    #[must_use]\n    pub fn dict_id(self) -> HeapId {\n        self.dict_id\n    }\n\n    /// Compares this items view to another items view using live dict item semantics.\n    pub(crate) fn eq_view(self, other: Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.dict_id == other.dict_id {\n            return Ok(true);\n        }\n\n        Heap::with_two(vm, self.dict_id, other.dict_id, |vm, left, right| {\n            let (HeapData::Dict(left), HeapData::Dict(right)) = (left, right) else {\n                panic!(\"dict_items view must always reference dicts\");\n            };\n            if left.len() != right.len() {\n                return Ok(false);\n            }\n            let token = vm.heap.incr_recursion_depth()?;\n            defer_drop!(token, vm);\n            for (key, value) in left {\n                vm.heap.check_time()?;\n                if let Ok(Some(other_v)) = right.get(key, vm) {\n                    if !value.py_eq(other_v, vm)? {\n                        return Ok(false);\n                    }\n                } else {\n                    return Ok(false);\n                }\n            }\n            Ok(true)\n        })\n    }\n\n    /// Compares this items view to a mutable set using set membership semantics.\n    pub(crate) fn eq_set(self, other: &Set, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Heap::with_entry_mut(vm, self.dict_id, |vm, data| {\n            let HeapDataMut::Dict(dict) = data else {\n                panic!(\"dict_items view must always reference a dict\");\n            };\n            dict_items_eq_set_like(\n                dict,\n                other.len(),\n                |item, vm| matches!(other.contains(item, vm), Ok(true)),\n                vm,\n            )\n        })\n    }\n\n    /// Compares this items view to a frozenset using set membership semantics.\n    pub(crate) fn eq_frozenset(\n        self,\n        other: &FrozenSet,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<bool, ResourceError> {\n        Heap::with_entry_mut(vm, self.dict_id, |vm, data| {\n            let HeapDataMut::Dict(dict) = data else {\n                panic!(\"dict_items view must always reference a dict\");\n            };\n            dict_items_eq_set_like(\n                dict,\n                other.len(),\n                |item, vm| matches!(other.contains(item, vm), Ok(true)),\n                vm,\n            )\n        })\n    }\n\n    /// Materializes the view's current live `(key, value)` pairs into a plain `set`.\n    ///\n    /// Each item is allocated as a 2-tuple so later set-like operators and\n    /// membership checks observe standard Python tuple semantics.\n    pub(crate) fn to_set(self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Set> {\n        Heap::with_entry_mut(vm, self.dict_id, |vm, data| {\n            let HeapDataMut::Dict(dict) = data else {\n                panic!(\"dict_items view must always reference a dict\");\n            };\n\n            let mut result = Set::with_capacity(dict.len());\n            for (key, value) in dict.iter() {\n                let item = allocate_tuple(smallvec![key.clone_with_heap(vm), value.clone_with_heap(vm)], vm.heap)?;\n                result.add(item, vm)?;\n            }\n            Ok(result)\n        })\n    }\n\n    /// Implements `dict_items.isdisjoint(iterable)` with CPython's iterable semantics.\n    pub(crate) fn isdisjoint_from_value(\n        self,\n        other: &Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<bool> {\n        let self_set = self.to_set(vm)?;\n        defer_drop!(self_set, vm);\n        let other_set = collect_iterable_to_set(other.clone_with_heap(vm), vm)?;\n        defer_drop!(other_set, vm);\n        sets_are_disjoint(self_set, other_set, vm)\n    }\n}\n\nimpl DictView for DictItemsView {\n    fn dict_id(&self) -> HeapId {\n        self.dict_id\n    }\n}\n\nimpl PyTrait for DictItemsView {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::DictItems\n    }\n\n    fn py_len(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.dict(vm.heap).len())\n    }\n\n    fn py_eq(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<bool, crate::resource::ResourceError> {\n        self.eq_view(*other, vm)\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        f.write_str(\"dict_items([\")?;\n        write_dict_items_contents(f, self.dict(vm.heap), vm, heap_ids)?;\n        f.write_str(\"])\")\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &crate::value::EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        match attr.static_string() {\n            Some(StaticStrings::Isdisjoint) => {\n                let other = args.get_one_arg(\"dict_items.isdisjoint\", vm.heap)?;\n                defer_drop!(other, vm);\n                Ok(CallResult::Value(Value::Bool(self.isdisjoint_from_value(other, vm)?)))\n            }\n            _ => Err(ExcType::attribute_error(Type::DictItems, attr.as_str(vm.interns))),\n        }\n    }\n}\n\nimpl HeapItem for DictItemsView {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        stack.push(self.dict_id);\n    }\n}\n\n/// Live view returned by `dict.values()`.\n///\n/// Unlike keys/items views, `dict_values` is intentionally not set-like in\n/// CPython. Milestone one only needs it to be a real view object with the same\n/// live iteration, repr, and membership behavior users expect from Python.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub(crate) struct DictValuesView {\n    dict_id: HeapId,\n}\n\nimpl DictValuesView {\n    /// Creates a new values view over an existing dictionary heap entry.\n    #[must_use]\n    pub fn new(dict_id: HeapId) -> Self {\n        Self { dict_id }\n    }\n\n    /// Returns the underlying dictionary heap id.\n    #[must_use]\n    pub fn dict_id(self) -> HeapId {\n        self.dict_id\n    }\n}\n\nimpl DictView for DictValuesView {\n    fn dict_id(&self) -> HeapId {\n        self.dict_id\n    }\n}\n\nimpl PyTrait for DictValuesView {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::DictValues\n    }\n\n    fn py_len(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.dict(vm.heap).len())\n    }\n\n    fn py_eq(\n        &self,\n        _other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<bool, crate::resource::ResourceError> {\n        Ok(false)\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        f.write_str(\"dict_values([\")?;\n        write_dict_values_contents(f, self.dict(vm.heap), vm, heap_ids)?;\n        f.write_str(\"])\")\n    }\n}\n\nimpl HeapItem for DictValuesView {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        stack.push(self.dict_id);\n    }\n}\n\n/// Compares two dicts for key-set equality using membership checks.\nfn dict_keys_eq_dict(\n    left: &Dict,\n    right: &Dict,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<bool, ResourceError> {\n    dict_keys_eq_set_like(\n        left,\n        right.len(),\n        |key, vm| matches!(right.get(key, vm), Ok(Some(_))),\n        vm,\n    )\n}\n\n/// Compares a dict's live keys to another set-like container by membership.\nfn dict_keys_eq_set_like<T: ResourceTracker>(\n    dict: &Dict,\n    other_len: usize,\n    mut contains: impl FnMut(&Value, &mut VM<'_, '_, T>) -> bool,\n    vm: &mut VM<'_, '_, T>,\n) -> Result<bool, ResourceError> {\n    if dict.len() != other_len {\n        return Ok(false);\n    }\n\n    let token = vm.heap.incr_recursion_depth()?;\n    defer_drop!(token, vm);\n    for (key, _) in dict {\n        vm.heap.check_time()?;\n        if !contains(key, vm) {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}\n\n/// Compares a dict's live items to another set-like container by membership.\nfn dict_items_eq_set_like<T: ResourceTracker>(\n    dict: &Dict,\n    other_len: usize,\n    mut contains: impl FnMut(&Value, &mut VM<'_, '_, T>) -> bool,\n    vm: &mut VM<'_, '_, T>,\n) -> Result<bool, ResourceError> {\n    if dict.len() != other_len {\n        return Ok(false);\n    }\n\n    let token = vm.heap.incr_recursion_depth()?;\n    defer_drop!(token, vm);\n    for (key, value) in dict {\n        vm.heap.check_time()?;\n        let item = allocate_tuple(smallvec![key.clone_with_heap(vm), value.clone_with_heap(vm)], vm.heap)?;\n        defer_drop!(item, vm);\n        if !contains(item, vm) {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}\n\n/// Writes the repr payload for a keys view without its outer wrapper.\nfn write_dict_keys_contents(\n    f: &mut impl Write,\n    dict: &Dict,\n    vm: &VM<'_, '_, impl ResourceTracker>,\n    heap_ids: &mut AHashSet<HeapId>,\n) -> std::fmt::Result {\n    let mut first = true;\n    for (key, _) in dict {\n        if !first {\n            f.write_str(\", \")?;\n        }\n        first = false;\n        key.py_repr_fmt(f, vm, heap_ids)?;\n    }\n    Ok(())\n}\n\n/// Writes the repr payload for an items view without its outer wrapper.\nfn write_dict_items_contents(\n    f: &mut impl Write,\n    dict: &Dict,\n    vm: &VM<'_, '_, impl ResourceTracker>,\n    heap_ids: &mut AHashSet<HeapId>,\n) -> std::fmt::Result {\n    let mut first = true;\n    for (key, value) in dict {\n        if !first {\n            f.write_str(\", \")?;\n        }\n        first = false;\n        f.write_char('(')?;\n        key.py_repr_fmt(f, vm, heap_ids)?;\n        f.write_str(\", \")?;\n        value.py_repr_fmt(f, vm, heap_ids)?;\n        f.write_char(')')?;\n    }\n    Ok(())\n}\n\n/// Writes the repr payload for a values view without its outer wrapper.\nfn write_dict_values_contents(\n    f: &mut impl Write,\n    dict: &Dict,\n    vm: &VM<'_, '_, impl ResourceTracker>,\n    heap_ids: &mut AHashSet<HeapId>,\n) -> std::fmt::Result {\n    let mut first = true;\n    for (_, value) in dict {\n        if !first {\n            f.write_str(\", \")?;\n        }\n        first = false;\n        value.py_repr_fmt(f, vm, heap_ids)?;\n    }\n    Ok(())\n}\n\n/// Collects an arbitrary iterable into a temporary `set`.\n///\n/// Dict-view operators accept any iterable on the right-hand side in CPython,\n/// including one-shot iterator objects. Reusing the same collection path keeps\n/// binary operators and `isdisjoint(...)` consistent with each other.\npub(crate) fn collect_iterable_to_set(\n    value: Value,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<Set, RunError> {\n    let is_existing_iterator =\n        matches!(&value, Value::Ref(heap_id) if matches!(vm.heap.get(*heap_id), HeapData::Iter(_)));\n\n    if is_existing_iterator {\n        let mut iterable_guard = crate::heap::HeapGuard::new(value, vm);\n        let (iterable, vm) = iterable_guard.as_parts_mut();\n        let Value::Ref(iter_id) = iterable else {\n            unreachable!(\"existing iterator check should guarantee a heap iterator\");\n        };\n        let mut set_guard = crate::heap::HeapGuard::new(Set::new(), vm);\n        let (set, vm) = set_guard.as_parts_mut();\n        while let Some(item) = advance_on_heap(vm.heap, *iter_id, vm.interns)? {\n            set.add(item, vm)?;\n        }\n        return Ok(set_guard.into_inner());\n    }\n\n    let iter = MontyIter::new(value, vm)?;\n    crate::defer_drop_mut!(iter, vm);\n    let mut set_guard = crate::heap::HeapGuard::new(Set::with_capacity(iter.size_hint(vm.heap)), vm);\n    let (set, vm) = set_guard.as_parts_mut();\n    while let Some(item) = iter.for_next(vm)? {\n        set.add(item, vm)?;\n    }\n    Ok(set_guard.into_inner())\n}\n\n/// Returns whether two temporary sets have no elements in common.\nfn sets_are_disjoint(left: &Set, right: &Set, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n    let (smaller, larger) = if left.len() <= right.len() {\n        (left, right)\n    } else {\n        (right, left)\n    };\n\n    for value in smaller.iter() {\n        if larger.contains(value, vm)? {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}\n"
  },
  {
    "path": "crates/monty/src/types/iter.rs",
    "content": "//! Iterator support for Python for loops and the `iter()` type constructor.\n//!\n//! This module provides the `MontyIter` struct which encapsulates iteration state\n//! for different iterable types. It uses index-based iteration internally to avoid\n//! borrow conflicts when accessing the heap during iteration.\n//!\n//! The design stores iteration state (indices) rather than Rust iterators, allowing\n//! `for_next()` to take `&mut Heap` for cloning values and allocating strings.\n//!\n//! For constructors like `list()` and `tuple()`, use `MontyIter::new()` followed\n//! by `collect()` to materialize all items into a Vec.\n//!\n//! ## Efficient Iteration with `IterState`\n//!\n//! For the VM's `ForIter` opcode, `advance_on_heap()` uses two strategies:\n//!\n//! **Fast path** for simple iterators (Range, InternBytes, ASCII IterStr):\n//! - Single `get_mut()` call to compute value and advance index\n//! - No additional heap access needed during iteration\n//!\n//! **Multi-phase approach** for complex iterators (IterStr, HeapRef):\n//! 1. `iter_state()` - reads current state without mutation, returns `Option<IterState>`\n//! 2. Get the value (may access other heap objects like strings or containers)\n//! 3. `advance()` - updates the index after the caller has done its work\n//!\n//! This allows `advance_on_heap()` to coordinate access without extracting\n//! the iterator from the heap (avoiding `std::mem::replace` overhead).\n//!\n//! ## Builtin Support\n//!\n//! The `iterator_next()` helper implements the `next()` builtin.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    exception_private::{ExcType, RunResult},\n    heap::{ContainsHeap, DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem},\n    heap_data::HeapDataMut,\n    intern::{BytesId, Interns, StringId},\n    resource::ResourceTracker,\n    types::{PyTrait, Range, dict_view::DictView, str::allocate_char},\n    value::Value,\n};\n\n/// Iterator state for Python for loops.\n///\n/// Contains the current iteration index and the type-specific iteration data.\n/// Uses index-based iteration to avoid borrow conflicts when accessing the heap.\n///\n/// For strings, stores the string content with a byte offset for O(1) UTF-8 iteration.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub struct MontyIter {\n    /// Current iteration index, shared across all iterator types.\n    index: usize,\n    /// Type-specific iteration data.\n    iter_value: IterValue,\n    /// the actual Value being iterated over.\n    value: Value,\n}\n\nimpl MontyIter {\n    /// Creates an iterator from the `iter()` constructor call.\n    ///\n    /// - `iter(iterable)` - Returns an iterator for the iterable. If the argument is\n    ///   already an iterator, returns the same object.\n    /// - `iter(callable, sentinel)` - Not yet supported.\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let (iterable, sentinel) = args.get_one_two_args(\"iter\", vm.heap)?;\n\n        if let Some(s) = sentinel {\n            // Two-argument form: iter(callable, sentinel)\n            // This is the sentinel iteration protocol, not yet supported\n            iterable.drop_with_heap(vm);\n            s.drop_with_heap(vm);\n            return Err(ExcType::type_error(\"iter(callable, sentinel) is not yet supported\"));\n        }\n\n        // Check if already an iterator - return self\n        if let Value::Ref(id) = &iterable\n            && matches!(vm.heap.get(*id), HeapData::Iter(_))\n        {\n            // Already an iterator - return it (refcount already correct from caller)\n            return Ok(iterable);\n        }\n\n        // Create new iterator\n        let iter = Self::new(iterable, vm)?;\n        let id = vm.heap.allocate(HeapData::Iter(iter))?;\n        Ok(Value::Ref(id))\n    }\n\n    /// Creates a new MontyIter from a Value.\n    ///\n    /// Returns an error if the value is not iterable.\n    /// For strings, copies the string content for byte-offset based iteration.\n    /// For ranges, the data is copied so the heap reference is dropped immediately.\n    pub fn new(mut value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        if let Some(iter_value) = IterValue::new(&value, vm) {\n            // For Range, we copy next/step/len into ForIterValue::Range, so we don't need\n            // to keep the heap object alive during iteration. Drop it immediately to avoid\n            // GC issues (the Range isn't in any namespace slot, so GC wouldn't see it).\n            // Same for IterStr which copies the string content.\n            if matches!(iter_value, IterValue::Range { .. } | IterValue::IterStr { .. }) {\n                value.drop_with_heap(vm);\n                value = Value::None;\n            }\n            Ok(Self {\n                index: 0,\n                iter_value,\n                value,\n            })\n        } else {\n            let err = ExcType::type_error_not_iterable(value.py_type(vm.heap));\n            value.drop_with_heap(vm);\n            Err(err)\n        }\n    }\n\n    /// Drops the iterator and its held value properly.\n    pub fn drop_with_heap(self, heap: &mut impl ContainsHeap) {\n        self.value.drop_with_heap(heap);\n    }\n\n    /// Collects HeapIds from this iterator for reference counting cleanup.\n    pub fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.value.py_dec_ref_ids(stack);\n    }\n\n    /// Returns whether this iterator holds a heap reference (`Value::Ref`).\n    ///\n    /// Used during allocation to determine if this container could create cycles.\n    #[inline]\n    #[must_use]\n    pub fn has_refs(&self) -> bool {\n        matches!(self.value, Value::Ref(_))\n    }\n\n    /// Returns a reference to the underlying value being iterated.\n    ///\n    /// Used by GC to traverse heap references held by the iterator.\n    pub fn value(&self) -> &Value {\n        &self.value\n    }\n\n    /// Returns the current iterator state without mutation.\n    ///\n    /// This is used by the multi-phase approach in `advance_on_heap()` for complex\n    /// iterator types (IterStr, HeapRef). Simple types (Range, InternBytes, ASCII\n    /// IterStr) are handled by the fast path and should not call this method.\n    ///\n    /// Returns `None` if the iterator is exhausted.\n    fn iter_state(&self) -> Option<IterState> {\n        match &self.iter_value {\n            // Range, InternBytes, and ASCII IterStr are handled by try_advance_simple() fast path\n            IterValue::Range { .. } | IterValue::InternBytes { .. } => {\n                unreachable!(\"Range and InternBytes use fast path, not iter_state\")\n            }\n            IterValue::IterStr {\n                string,\n                byte_offset,\n                len,\n                ..\n            } => {\n                if self.index >= *len {\n                    None\n                } else {\n                    // Get the next character at current byte offset\n                    let c = string[*byte_offset..]\n                        .chars()\n                        .next()\n                        .expect(\"index < len implies char exists\");\n                    Some(IterState::IterStr {\n                        char: c,\n                        char_len: c.len_utf8(),\n                    })\n                }\n            }\n            IterValue::HeapRef {\n                heap_id,\n                len,\n                checks_mutation,\n            } => {\n                // For types with captured len, check exhaustion here.\n                // For List (len=None), exhaustion is checked in advance_on_heap().\n                if let Some(l) = len\n                    && self.index >= *l\n                {\n                    return None;\n                }\n                Some(IterState::HeapIndex {\n                    heap_id: *heap_id,\n                    index: self.index,\n                    expected_len: if *checks_mutation { *len } else { None },\n                })\n            }\n        }\n    }\n\n    /// Advances the iterator by one step.\n    ///\n    /// This is phase 2 of the two-phase iteration approach. Call this after\n    /// successfully retrieving the value using the data from `iter_state()`.\n    ///\n    /// For string iterators, `string_char_len` must be provided (the UTF-8 byte\n    /// length of the character that was just yielded) to update the byte offset.\n    /// For other iterator types, pass `None`.\n    #[inline]\n    pub fn advance(&mut self, string_char_len: Option<usize>) {\n        self.index += 1;\n        if let Some(char_len) = string_char_len\n            && let IterValue::IterStr { byte_offset, .. } = &mut self.iter_value\n        {\n            *byte_offset += char_len;\n        }\n    }\n\n    /// Attempts to advance simple iterator types that don't need additional heap access.\n    ///\n    /// Returns `Some(result)` if handled (Range, InternBytes, ASCII IterStr),\n    /// `None` if caller should use the multi-phase approach (non-ASCII IterStr, HeapRef).\n    ///\n    /// This optimization avoids two heap lookups for iterator types that can compute\n    /// their next value without accessing other heap objects.\n    #[inline]\n    fn try_advance_simple(&mut self, interns: &Interns) -> Option<RunResult<Option<Value>>> {\n        match &mut self.iter_value {\n            IterValue::Range { next, step, len } => {\n                if self.index >= *len {\n                    Some(Ok(None))\n                } else {\n                    let value = *next;\n                    *next += *step;\n                    self.index += 1;\n                    Some(Ok(Some(Value::Int(value))))\n                }\n            }\n            IterValue::IterStr {\n                string,\n                byte_offset,\n                len,\n                is_ascii,\n            } => {\n                if !*is_ascii {\n                    None\n                } else if self.index >= *len {\n                    Some(Ok(None))\n                } else {\n                    let byte = string.as_bytes()[*byte_offset];\n                    *byte_offset += 1;\n                    self.index += 1;\n                    Some(Ok(Some(Value::InternString(StringId::from_ascii(byte)))))\n                }\n            }\n            IterValue::InternBytes { bytes_id, len } => {\n                if self.index >= *len {\n                    Some(Ok(None))\n                } else {\n                    let i = self.index;\n                    self.index += 1;\n                    let bytes = interns.get_bytes(*bytes_id);\n                    Some(Ok(Some(Value::Int(i64::from(bytes[i])))))\n                }\n            }\n            IterValue::HeapRef { .. } => None,\n        }\n    }\n\n    /// Returns the next item from the iterator, advancing the internal index.\n    ///\n    /// Returns `Ok(None)` when the iterator is exhausted.\n    /// Returns `Err` if allocation fails (for string character iteration) or if\n    /// a dict/set changes size during iteration (RuntimeError).\n    pub fn for_next(&mut self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        // Check timeout on every iteration step. For NoLimitTracker this is\n        // inlined as a no-op. For LimitTracker it ensures that Rust-side loops\n        // (sum, sorted, min, max, etc.) cannot bypass the VM's per-instruction\n        // timeout check by running entirely within a single bytecode instruction.\n        vm.heap.check_time()?;\n        match &mut self.iter_value {\n            IterValue::Range { next, step, len } => {\n                if self.index >= *len {\n                    return Ok(None);\n                }\n                let value = *next;\n                *next += *step;\n                self.index += 1;\n                Ok(Some(Value::Int(value)))\n            }\n            IterValue::IterStr {\n                string,\n                byte_offset,\n                len,\n                is_ascii,\n            } => {\n                if self.index >= *len {\n                    Ok(None)\n                } else if *is_ascii {\n                    let byte = string.as_bytes()[*byte_offset];\n                    *byte_offset += 1;\n                    self.index += 1;\n                    Ok(Some(Value::InternString(StringId::from_ascii(byte))))\n                } else {\n                    // Get next char at current byte offset\n                    let c = string[*byte_offset..]\n                        .chars()\n                        .next()\n                        .expect(\"index < len implies char exists\");\n                    *byte_offset += c.len_utf8();\n                    self.index += 1;\n                    Ok(Some(allocate_char(c, vm.heap)?))\n                }\n            }\n            IterValue::InternBytes { bytes_id, len } => {\n                if self.index >= *len {\n                    return Ok(None);\n                }\n                let i = self.index;\n                self.index += 1;\n                let bytes = vm.interns.get_bytes(*bytes_id);\n                Ok(Some(Value::Int(i64::from(bytes[i]))))\n            }\n            IterValue::HeapRef {\n                heap_id,\n                len,\n                checks_mutation,\n            } => {\n                // Check exhaustion for types with captured len\n                if let Some(l) = len\n                    && self.index >= *l\n                {\n                    return Ok(None);\n                }\n                let i = self.index;\n                let expected_len = if *checks_mutation { *len } else { None };\n                let item = get_heap_item(vm.heap, *heap_id, i, expected_len)?;\n                // Check for list exhaustion (list can shrink during iteration)\n                let Some(item) = item else {\n                    return Ok(None);\n                };\n                self.index += 1;\n                Ok(Some(item))\n            }\n        }\n    }\n\n    /// Returns the remaining size for iterables based on current state.\n    ///\n    /// For immutable types (Range, Tuple, Str, Bytes, FrozenSet), returns the exact remaining count.\n    /// For List, returns current length minus index (may change if list is mutated).\n    /// For Dict and Set, returns the captured length minus index (used for size-change detection).\n    pub fn size_hint(&self, heap: &Heap<impl ResourceTracker>) -> usize {\n        let len = match &self.iter_value {\n            IterValue::Range { len, .. } | IterValue::IterStr { len, .. } | IterValue::InternBytes { len, .. } => *len,\n            IterValue::HeapRef { heap_id, len, .. } => {\n                // For List (len=None), check current length dynamically\n                len.unwrap_or_else(|| {\n                    let HeapData::List(list) = heap.get(*heap_id) else {\n                        panic!(\"HeapRef with len=None should only be List\")\n                    };\n                    list.len()\n                })\n            }\n        };\n        len.saturating_sub(self.index)\n    }\n\n    /// Collects all remaining items from the iterator into a Vec.\n    ///\n    /// Consumes the iterator and returns all items. Used by `list()`, `tuple()`,\n    /// and similar constructors that need to materialize all items.\n    ///\n    /// Pre-allocates capacity based on `size_hint()` for better performance.\n    pub fn collect<T: FromIterator<Value>>(self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<T> {\n        let mut guard = HeapGuard::new(self, vm);\n        let (this, vm) = guard.as_parts_mut();\n        HeapedMontyIter(this, vm).collect()\n    }\n}\n\nstruct HeapedMontyIter<'this, 'a, 'p, T: ResourceTracker>(&'this mut MontyIter, &'this mut VM<'a, 'p, T>);\n\nimpl<T: ResourceTracker> Iterator for HeapedMontyIter<'_, '_, '_, T> {\n    type Item = RunResult<Value>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.0.for_next(self.1).transpose()\n    }\n\n    fn size_hint(&self) -> (usize, Option<usize>) {\n        let remaining = self.0.size_hint(self.1.heap);\n        (remaining, Some(remaining))\n    }\n}\n\n/// Advances an iterator stored on the heap and returns the next value.\n///\n/// Uses a fast path for simple iterators (Range, InternBytes, ASCII IterStr) that don't need\n/// additional heap access - these are handled with a single mutable borrow.\n///\n/// For complex iterators (IterStr, HeapRef), uses a multi-phase approach:\n/// 1. Read iterator state (immutable borrow ends)\n/// 2. Based on state, get the value (may access other heap objects)\n/// 3. Update iterator index (mutable borrow)\n///\n/// This is more efficient than `std::mem::replace` with a placeholder because\n/// it avoids creating and moving placeholder objects on every iteration.\n///\n/// Returns `Ok(None)` when the iterator is exhausted.\n/// Returns `Err` for dict/set size changes or allocation failures.\npub(crate) fn advance_on_heap(\n    heap: &mut Heap<impl ResourceTracker>,\n    iter_id: HeapId,\n    interns: &Interns,\n) -> RunResult<Option<Value>> {\n    // Fast path: Range and InternBytes don't need additional heap access,\n    // so we can handle them with a single mutable borrow.\n    {\n        let HeapDataMut::Iter(iter) = heap.get_mut(iter_id) else {\n            panic!(\"advance_on_heap: expected Iterator on heap\");\n        };\n        if let Some(result) = iter.try_advance_simple(interns) {\n            return result;\n        }\n    }\n    // Mutable borrow ends here, allowing the multi-phase approach below\n\n    // Multi-phase approach for IterStr and HeapRef (need heap access during value retrieval)\n    // Phase 1: Get iterator state (immutable borrow ends after this block)\n    let HeapData::Iter(iter) = heap.get(iter_id) else {\n        panic!(\"advance_on_heap: expected Iterator on heap\");\n    };\n    let Some(state) = iter.iter_state() else {\n        return Ok(None); // Iterator exhausted\n    };\n\n    // Phase 2: Based on state, get the value and determine char_len for strings\n    let (value, string_char_len) = match state {\n        IterState::IterStr { char, char_len } => {\n            let value = allocate_char(char, heap)?;\n            (value, Some(char_len))\n        }\n        IterState::HeapIndex {\n            heap_id,\n            index,\n            expected_len,\n        } => {\n            let item = get_heap_item(heap, heap_id, index, expected_len)?;\n            // Check for list exhaustion (list can shrink during iteration)\n            let Some(item) = item else {\n                return Ok(None);\n            };\n            (item, None)\n        }\n    };\n\n    // Phase 3: Advance the iterator\n    let HeapDataMut::Iter(iter) = heap.get_mut(iter_id) else {\n        panic!(\"advance_on_heap: expected Iterator on heap\");\n    };\n    iter.advance(string_char_len);\n\n    Ok(Some(value))\n}\n\n/// Gets an item from a heap-allocated container at the given index.\n///\n/// Returns `Ok(None)` if the index is out of bounds (for lists that shrunk during iteration).\n/// Returns `Err` if a dict/set changed size during iteration (RuntimeError).\nfn get_heap_item(\n    heap: &mut Heap<impl ResourceTracker>,\n    heap_id: HeapId,\n    index: usize,\n    expected_len: Option<usize>,\n) -> RunResult<Option<Value>> {\n    match heap.get(heap_id) {\n        HeapData::List(list) => {\n            // Check if list shrunk during iteration\n            if index >= list.len() {\n                return Ok(None);\n            }\n            Ok(Some(list.as_slice()[index].clone_with_heap(heap)))\n        }\n        HeapData::Tuple(tuple) => Ok(Some(tuple.as_slice()[index].clone_with_heap(heap))),\n        HeapData::NamedTuple(namedtuple) => Ok(Some(namedtuple.as_vec()[index].clone_with_heap(heap))),\n        HeapData::Dict(dict) => {\n            // Check for dict mutation\n            if let Some(expected) = expected_len\n                && dict.len() != expected\n            {\n                return Err(ExcType::runtime_error_dict_changed_size());\n            }\n            Ok(Some(\n                dict.key_at(index).expect(\"index should be valid\").clone_with_heap(heap),\n            ))\n        }\n        HeapData::DictKeysView(view) => {\n            let dict = view.dict(heap);\n            if let Some(expected) = expected_len\n                && dict.len() != expected\n            {\n                return Err(ExcType::runtime_error_dict_changed_size());\n            }\n            Ok(Some(\n                dict.key_at(index).expect(\"index should be valid\").clone_with_heap(heap),\n            ))\n        }\n        HeapData::DictItemsView(view) => {\n            let dict = view.dict(heap);\n            if let Some(expected) = expected_len\n                && dict.len() != expected\n            {\n                return Err(ExcType::runtime_error_dict_changed_size());\n            }\n            let (key, value) = dict.item_at(index).expect(\"index should be valid\");\n            Ok(Some(crate::types::allocate_tuple(\n                smallvec::smallvec![key.clone_with_heap(heap), value.clone_with_heap(heap)],\n                heap,\n            )?))\n        }\n        HeapData::DictValuesView(view) => {\n            let dict = view.dict(heap);\n            if let Some(expected) = expected_len\n                && dict.len() != expected\n            {\n                return Err(ExcType::runtime_error_dict_changed_size());\n            }\n            Ok(Some(\n                dict.value_at(index)\n                    .expect(\"index should be valid\")\n                    .clone_with_heap(heap),\n            ))\n        }\n        HeapData::Bytes(bytes) => Ok(Some(Value::Int(i64::from(bytes.as_slice()[index])))),\n        HeapData::Set(set) => {\n            // Check for set mutation\n            if let Some(expected) = expected_len\n                && set.len() != expected\n            {\n                return Err(ExcType::runtime_error_set_changed_size());\n            }\n            Ok(Some(\n                set.storage()\n                    .value_at(index)\n                    .expect(\"index should be valid\")\n                    .clone_with_heap(heap),\n            ))\n        }\n        HeapData::FrozenSet(frozenset) => Ok(Some(\n            frozenset\n                .storage()\n                .value_at(index)\n                .expect(\"index should be valid\")\n                .clone_with_heap(heap),\n        )),\n        _ => panic!(\"get_heap_item: unexpected heap data type\"),\n    }\n}\n\n/// Gets the next item from an iterator.\n///\n/// If the iterator is exhausted:\n/// - If `default` is `Some`, returns the default value\n/// - If `default` is `None`, raises `StopIteration`\n///\n/// This implements Python's `next()` builtin semantics.\n///\n/// # Arguments\n/// * `iter_value` - Must be an iterator (heap-allocated MontyIter)\n/// * `default` - Optional default value to return when exhausted\n/// * `heap` - The heap for memory operations\n/// * `interns` - String interning table\n///\n/// # Errors\n/// Returns `StopIteration` if exhausted with no default, or propagates errors from iteration.\npub fn iterator_next(\n    iter_value: &Value,\n    default: Option<Value>,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Value> {\n    let Value::Ref(iter_id) = iter_value else {\n        // Not a heap value - can't be an iterator\n        if let Some(d) = default {\n            d.drop_with_heap(heap);\n        }\n        return Err(ExcType::type_error_not_iterable(iter_value.py_type(heap)));\n    };\n\n    // Check that it's actually an iterator\n    if !matches!(heap.get(*iter_id), HeapData::Iter(_)) {\n        if let Some(d) = default {\n            d.drop_with_heap(heap);\n        }\n        let data_type = heap.get(*iter_id).py_type(heap);\n        return Err(ExcType::type_error(format!(\"'{data_type}' object is not an iterator\")));\n    }\n\n    // Get next item using the MontyIter::advance_on_heap method\n    match advance_on_heap(heap, *iter_id, interns)? {\n        Some(item) => {\n            // Drop default if provided since we don't need it\n            if let Some(d) = default {\n                d.drop_with_heap(heap);\n            }\n            Ok(item)\n        }\n        None => {\n            // Iterator exhausted\n            match default {\n                Some(d) => Ok(d),\n                None => Err(ExcType::stop_iteration()),\n            }\n        }\n    }\n}\n\n/// Snapshot of iterator state needed to produce the next value.\n///\n/// This enum captures state for complex iterator types (IterStr, HeapRef) that\n/// require the multi-phase approach in `advance_on_heap()`. Simple types (Range,\n/// InternBytes, ASCII IterStr) are handled by the fast path and don't use this enum.\n///\n/// The multi-phase approach avoids borrow conflicts:\n/// 1. Read `Option<IterState>` from iterator (immutable borrow ends, `None` means exhausted)\n/// 2. Use the state to get the value (may access other heap objects)\n/// 3. Call `advance()` to update the iterator index\n#[derive(Debug, Clone, Copy)]\nenum IterState {\n    /// String iterator yields this character; char_len is UTF-8 byte length for advance().\n    IterStr { char: char, char_len: usize },\n    /// Heap-based iterator (List, Tuple, NamedTuple, Dict, Bytes, Set, FrozenSet).\n    /// The expected_len is Some for types that check for mutation (Dict, Set).\n    HeapIndex {\n        heap_id: HeapId,\n        index: usize,\n        expected_len: Option<usize>,\n    },\n}\n\n/// Type-specific iteration data for different Python iterable types.\n///\n/// Each variant stores the data needed to iterate over a specific type,\n/// excluding the index which is stored in the parent `MontyIter` struct.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\nenum IterValue {\n    /// Iterating over a Range, yields `Value::Int`.\n    Range {\n        /// Next value to yield.\n        next: i64,\n        /// Step between values.\n        step: i64,\n        /// Total number of elements.\n        len: usize,\n    },\n    /// Iterating over a string (heap or interned), yields single-char Str values.\n    ///\n    /// Stores a copy of the string content plus a byte offset for O(1) UTF-8 character access.\n    /// We store the string rather than referencing the heap because `for_next()` needs mutable\n    /// heap access to allocate the returned character strings, which would conflict with\n    /// borrowing the source string from the heap.\n    IterStr {\n        /// Copy of the string content for iteration.\n        string: String,\n        /// Current byte offset into the string (points to next char to yield).\n        byte_offset: usize,\n        /// Total number of characters in the string.\n        len: usize,\n        /// Whether the string is ASCII (enables fast-path iteration).\n        is_ascii: bool,\n    },\n    /// Iterating over interned bytes, yields `Value::Int` for each byte.\n    InternBytes { bytes_id: BytesId, len: usize },\n    /// Iterating over a heap-allocated container (List, Tuple, NamedTuple, Dict, Bytes, Set, FrozenSet).\n    ///\n    /// - `len`: `None` for List (checked dynamically since lists can mutate during iteration),\n    ///   `Some(n)` for other types (captured at construction for exhaustion checking).\n    /// - `checks_mutation`: `true` for Dict/Set (raises RuntimeError if size changes),\n    ///   `false` for other types.\n    HeapRef {\n        heap_id: HeapId,\n        len: Option<usize>,\n        checks_mutation: bool,\n    },\n}\n\nimpl IterValue {\n    fn new(value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Option<Self> {\n        match &value {\n            Value::InternString(string_id) => Some(Self::from_str(vm.interns.get_str(*string_id))),\n            Value::InternBytes(bytes_id) => Some(Self::from_intern_bytes(*bytes_id, vm.interns)),\n            Value::Ref(heap_id) => Self::from_heap_data(*heap_id, vm.heap),\n            _ => None,\n        }\n    }\n\n    /// Creates a Range iterator value.\n    fn from_range(range: &Range) -> Self {\n        Self::Range {\n            next: range.start,\n            step: range.step,\n            len: range.len(),\n        }\n    }\n\n    /// Creates an iterator value over a string.\n    ///\n    /// Copies the string content and counts characters for the length field.\n    fn from_str(s: &str) -> Self {\n        let is_ascii = s.is_ascii();\n        let len = if is_ascii { s.len() } else { s.chars().count() };\n        Self::IterStr {\n            string: s.to_owned(),\n            byte_offset: 0,\n            len,\n            is_ascii,\n        }\n    }\n\n    /// Creates an iterator value over interned bytes.\n    fn from_intern_bytes(bytes_id: BytesId, interns: &Interns) -> Self {\n        let bytes = interns.get_bytes(bytes_id);\n        Self::InternBytes {\n            bytes_id,\n            len: bytes.len(),\n        }\n    }\n\n    /// Creates an iterator value from heap data.\n    fn from_heap_data(heap_id: HeapId, heap: &Heap<impl ResourceTracker>) -> Option<Self> {\n        match heap.get(heap_id) {\n            // List: no captured len (checked dynamically), no mutation check\n            HeapData::List(_) => Some(Self::HeapRef {\n                heap_id,\n                len: None,\n                checks_mutation: false,\n            }),\n            // Tuple/NamedTuple/Bytes/FrozenSet: captured len, no mutation check\n            HeapData::Tuple(tuple) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(tuple.as_slice().len()),\n                checks_mutation: false,\n            }),\n            HeapData::NamedTuple(namedtuple) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(namedtuple.len()),\n                checks_mutation: false,\n            }),\n            HeapData::Bytes(b) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(b.len()),\n                checks_mutation: false,\n            }),\n            HeapData::FrozenSet(frozenset) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(frozenset.len()),\n                checks_mutation: false,\n            }),\n            // Dict and dict views: captured len, WITH mutation check\n            HeapData::Dict(dict) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(dict.len()),\n                checks_mutation: true,\n            }),\n            HeapData::DictKeysView(view) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(view.dict(heap).len()),\n                checks_mutation: true,\n            }),\n            HeapData::DictItemsView(view) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(view.dict(heap).len()),\n                checks_mutation: true,\n            }),\n            HeapData::DictValuesView(view) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(view.dict(heap).len()),\n                checks_mutation: true,\n            }),\n            HeapData::Set(set) => Some(Self::HeapRef {\n                heap_id,\n                len: Some(set.len()),\n                checks_mutation: true,\n            }),\n            // String: copy content for iteration\n            HeapData::Str(s) => Some(Self::from_str(s.as_str())),\n            // Range: copy values for iteration\n            HeapData::Range(range) => Some(Self::from_range(range)),\n            // other types are not iterable\n            _ => None,\n        }\n    }\n}\n\nimpl DropWithHeap for MontyIter {\n    #[inline]\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        Self::drop_with_heap(self, heap);\n    }\n}\n\nimpl HeapItem for MontyIter {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.value.py_dec_ref_ids(stack);\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/list.rs",
    "content": "use std::fmt::Write;\n\nuse ahash::AHashSet;\nuse itertools::Itertools;\nuse smallvec::SmallVec;\n\nuse super::{MontyIter, PyTrait};\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunError, RunResult},\n    heap::{DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem},\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    sorting::{apply_permutation, sort_indices},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Python list type, wrapping a Vec of Values.\n///\n/// This type provides Python list semantics including dynamic growth,\n/// reference counting for heap values, and standard list methods.\n///\n/// # Implemented Methods\n/// - `append(item)` - Add item to end\n/// - `insert(index, item)` - Insert item at index\n/// - `pop([index])` - Remove and return item (default: last)\n/// - `remove(value)` - Remove first occurrence of value\n/// - `clear()` - Remove all items\n/// - `copy()` - Shallow copy\n/// - `extend(iterable)` - Append items from iterable\n/// - `index(value[, start[, end]])` - Find first index of value\n/// - `count(value)` - Count occurrences\n/// - `reverse()` - Reverse in place\n/// - `sort([key][, reverse])` - Sort in place\n///\n/// Note: `sort(key=...)` supports builtin key functions (len, abs, etc.)\n/// but not user-defined functions. This is handled at VM level for access\n/// to function calling machinery.\n///\n/// All list methods from Python's builtins are implemented.\n///\n/// # Reference Counting\n/// When values are added to the list (via append, insert, etc.), their\n/// reference counts are incremented if they are heap-allocated (Ref variants).\n/// This ensures values remain valid while referenced by the list.\n///\n/// # GC Optimization\n/// The `contains_refs` flag tracks whether the list contains any `Value::Ref` items.\n/// This allows `collect_child_ids` and `py_dec_ref_ids` to skip iteration when the\n/// list contains only primitive values (ints, bools, None, etc.), significantly\n/// improving GC performance for lists of primitives.\n#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]\npub(crate) struct List {\n    items: Vec<Value>,\n    /// True if any item in the list is a `Value::Ref`. Used to skip iteration\n    /// in `collect_child_ids` and `py_dec_ref_ids` when no refs are present.\n    contains_refs: bool,\n}\n\nimpl List {\n    /// Creates a new list from a vector of values.\n    ///\n    /// Automatically computes the `contains_refs` flag by checking if any value\n    /// is a `Value::Ref`.\n    ///\n    /// Note: This does NOT increment reference counts - the caller must\n    /// ensure refcounts are properly managed.\n    #[must_use]\n    pub fn new(vec: Vec<Value>) -> Self {\n        let contains_refs = vec.iter().any(|v| matches!(v, Value::Ref(_)));\n        Self {\n            items: vec,\n            contains_refs,\n        }\n    }\n\n    /// Returns a reference to the underlying vector.\n    #[must_use]\n    pub fn as_slice(&self) -> &[Value] {\n        &self.items\n    }\n\n    /// Returns a mutable reference to the underlying vector.\n    ///\n    /// # Safety Considerations\n    /// Be careful when mutating the vector directly - you must manually\n    /// manage reference counts for any heap values you add or remove.\n    /// The `contains_refs` flag is NOT automatically updated by direct\n    /// vector mutations. Prefer using `append()` or `insert()` instead.\n    pub fn as_vec_mut(&mut self) -> &mut Vec<Value> {\n        &mut self.items\n    }\n\n    /// Returns the number of elements in the list.\n    #[must_use]\n    pub fn len(&self) -> usize {\n        self.items.len()\n    }\n\n    /// Returns whether the list contains any heap references.\n    ///\n    /// When false, `collect_child_ids` and `py_dec_ref_ids` can skip iteration.\n    #[inline]\n    #[must_use]\n    pub fn contains_refs(&self) -> bool {\n        self.contains_refs\n    }\n\n    /// Marks that the list contains heap references.\n    ///\n    /// This should be called when directly mutating the list's items vector\n    /// (via `as_vec_mut()`) with values that include `Value::Ref` variants.\n    #[inline]\n    pub fn set_contains_refs(&mut self) {\n        self.contains_refs = true;\n    }\n\n    /// Appends an element to the end of the list.\n    ///\n    /// The caller transfers ownership of `item` to the list. The item's refcount\n    /// is NOT incremented here - the caller is responsible for ensuring the refcount\n    /// was already incremented (e.g., via `clone_with_heap` or `evaluate_use`).\n    ///\n    /// Returns `Value::None`, matching Python's behavior where `list.append()` returns None.\n    pub fn append(&mut self, heap: &mut Heap<impl ResourceTracker>, item: Value) {\n        // Track if we're adding a reference and mark potential cycle\n        if matches!(item, Value::Ref(_)) {\n            self.contains_refs = true;\n            heap.mark_potential_cycle();\n        }\n        // Ownership transfer - refcount was already handled by caller\n        self.items.push(item);\n    }\n\n    /// Inserts an element at the specified index.\n    ///\n    /// The caller transfers ownership of `item` to the list. The item's refcount\n    /// is NOT incremented here - the caller is responsible for ensuring the refcount\n    /// was already incremented.\n    ///\n    /// # Arguments\n    /// * `index` - The position to insert at (0-based). If index >= len(),\n    ///   the item is appended to the end (matching Python semantics).\n    ///\n    /// Returns `Value::None`, matching Python's behavior where `list.insert()` returns None.\n    pub fn insert(&mut self, heap: &mut Heap<impl ResourceTracker>, index: usize, item: Value) {\n        // Track if we're adding a reference and mark potential cycle\n        if matches!(item, Value::Ref(_)) {\n            self.contains_refs = true;\n            heap.mark_potential_cycle();\n        }\n        // Ownership transfer - refcount was already handled by caller\n        // Python's insert() appends if index is out of bounds\n        if index >= self.items.len() {\n            self.items.push(item);\n        } else {\n            self.items.insert(index, item);\n        }\n    }\n\n    /// Creates a list from the `list()` constructor call.\n    ///\n    /// - `list()` with no args returns an empty list\n    /// - `list(iterable)` creates a list from any iterable (list, tuple, range, str, bytes, dict)\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let value = args.get_zero_one_arg(\"list\", vm.heap)?;\n        match value {\n            None => {\n                let heap_id = vm.heap.allocate(HeapData::List(Self::new(Vec::new())))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(v) => {\n                let items = MontyIter::new(v, vm)?.collect(vm)?;\n                let heap_id = vm.heap.allocate(HeapData::List(Self::new(items)))?;\n                Ok(Value::Ref(heap_id))\n            }\n        }\n    }\n\n    /// Handles slice-based indexing for lists.\n    ///\n    /// Returns a new list containing the selected elements.\n    fn getitem_slice(&self, slice: &crate::types::Slice, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let (start, stop, step) = slice\n            .indices(self.items.len())\n            .map_err(|()| ExcType::value_error_slice_step_zero())?;\n\n        let items = get_slice_items(&self.items, start, stop, step, heap)?;\n        let heap_id = heap.allocate(HeapData::List(Self::new(items)))?;\n        Ok(Value::Ref(heap_id))\n    }\n}\n\nimpl From<List> for Vec<Value> {\n    fn from(list: List) -> Self {\n        list.items\n    }\n}\n\nimpl PyTrait for List {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::List\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.items.len())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        // Check for slice first (Value::Ref pointing to HeapData::Slice)\n        if let Value::Ref(id) = key\n            && let HeapData::Slice(slice) = heap.get(*id)\n        {\n            // Clone the slice to release the borrow on heap before calling getitem_slice\n            let slice = slice.clone();\n            return self.getitem_slice(&slice, heap);\n        }\n\n        // Extract integer index, accepting Int, Bool (True=1, False=0), and LongInt\n        let index = key.as_index(heap, Type::List)?;\n\n        // Convert to usize, handling negative indices (Python-style: -1 = last element)\n        let len = i64::try_from(self.items.len()).expect(\"list length exceeds i64::MAX\");\n        let normalized_index = if index < 0 { index + len } else { index };\n\n        // Bounds check\n        if normalized_index < 0 || normalized_index >= len {\n            return Err(ExcType::list_index_error());\n        }\n\n        // Return clone of the item with proper refcount increment\n        // Safety: normalized_index is validated to be in [0, len) above\n        let idx = usize::try_from(normalized_index).expect(\"list index validated non-negative\");\n        Ok(self.items[idx].clone_with_heap(heap))\n    }\n\n    fn py_setitem(&mut self, key: Value, value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        let heap = &mut *vm.heap;\n        defer_drop!(key, heap);\n        defer_drop_mut!(value, heap);\n\n        // Extract integer index, accepting Int, Bool (True=1, False=0), and LongInt.\n        // Note: The LongInt-to-i64 conversion is defensive code. In normal execution,\n        // heap-allocated LongInt values always exceed i64 range because into_value()\n        // demotes i64-fitting values to Value::Int. However, this could be reached via\n        // deserialization of crafted snapshot data.\n        let index = match key {\n            Value::Int(i) => *i,\n            Value::Bool(b) => i64::from(*b),\n            Value::Ref(heap_id) => {\n                if let HeapData::LongInt(li) = heap.get(*heap_id) {\n                    if let Some(i) = li.to_i64() {\n                        i\n                    } else {\n                        return Err(ExcType::index_error_int_too_large());\n                    }\n                } else {\n                    let key_type = key.py_type(heap);\n                    return Err(ExcType::type_error_list_assignment_indices(key_type));\n                }\n            }\n            _ => {\n                let key_type = key.py_type(heap);\n                return Err(ExcType::type_error_list_assignment_indices(key_type));\n            }\n        };\n\n        // Normalize negative indices (Python-style: -1 = last element)\n        let len = i64::try_from(self.items.len()).expect(\"list length exceeds i64::MAX\");\n        let normalized_index = if index < 0 { index + len } else { index };\n\n        // Bounds check\n        if normalized_index < 0 || normalized_index >= len {\n            return Err(ExcType::list_assignment_index_error());\n        }\n\n        let idx = usize::try_from(normalized_index).expect(\"index validated non-negative\");\n\n        // Update contains_refs if storing a Ref (must check before swap,\n        // since after swap `value` holds the old item)\n        if matches!(*value, Value::Ref(_)) {\n            self.contains_refs = true;\n            heap.mark_potential_cycle();\n        }\n\n        // Replace value (old one dropped by defer_drop_mut guard)\n        std::mem::swap(&mut self.items[idx], value);\n\n        Ok(())\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.items.len() != other.items.len() {\n            return Ok(false);\n        }\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        for (i1, i2) in self.items.iter().zip(&other.items) {\n            vm.heap.check_time()?;\n            if !i1.py_eq(i2, vm)? {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.items.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        repr_sequence_fmt('[', ']', &self.items, f, vm, heap_ids)\n    }\n\n    fn py_add(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, crate::resource::ResourceError> {\n        let heap = &mut *vm.heap;\n        // Clone both lists' contents with proper refcounting\n        let mut result: Vec<Value> = self.items.iter().map(|obj| obj.clone_with_heap(heap)).collect();\n        let other_cloned: Vec<Value> = other.items.iter().map(|obj| obj.clone_with_heap(heap)).collect();\n        result.extend(other_cloned);\n        let id = heap.allocate(HeapData::List(Self::new(result)))?;\n        Ok(Some(Value::Ref(id)))\n    }\n\n    fn py_iadd(\n        &mut self,\n        other: &Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        self_id: Option<HeapId>,\n    ) -> Result<bool, crate::resource::ResourceError> {\n        let heap = &mut *vm.heap;\n        // Extract the value ID first, keeping `other` around to drop later\n        let Value::Ref(other_id) = other else { return Ok(false) };\n\n        if Some(*other_id) == self_id {\n            // Self-extend: clone our own items with proper refcounting\n            let items = self\n                .items\n                .iter()\n                .map(|obj| obj.clone_with_heap(heap))\n                .collect::<Vec<_>>();\n            // If we're self-extending and have refs, mark potential cycle\n            if self.contains_refs {\n                heap.mark_potential_cycle();\n            }\n            self.items.extend(items);\n        } else {\n            // Get items from other list using iadd_extend_from_heap helper\n            // This handles the borrow checker limitations with lifetime propagation\n            let prev_len = self.items.len();\n            if !heap.iadd_extend_list(*other_id, &mut self.items) {\n                return Ok(false);\n            }\n            // Check if we added any refs and mark potential cycle\n            if self.contains_refs {\n                // Already had refs, but adding more may create cycles\n                heap.mark_potential_cycle();\n            } else {\n                for item in &self.items[prev_len..] {\n                    if matches!(item, Value::Ref(_)) {\n                        self.contains_refs = true;\n                        heap.mark_potential_cycle();\n                        break;\n                    }\n                }\n            }\n        }\n\n        Ok(true)\n    }\n\n    /// Intercepts `sort` to call `do_list_sort` (which needs `PrintWriter` for key functions),\n    /// and delegates all other methods to `call_list_method`.\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        if attr.static_string() == Some(StaticStrings::Sort) {\n            do_list_sort(self, args, vm)?;\n            return Ok(CallResult::Value(Value::None));\n        }\n        let args_guard = HeapGuard::new(args, vm.heap);\n        let Some(method) = attr.static_string() else {\n            return Err(ExcType::attribute_error(Type::List, attr.as_str(vm.interns)));\n        };\n\n        let args = args_guard.into_inner();\n        call_list_method(self, method, args, vm).map(CallResult::Value)\n    }\n}\n\nimpl HeapItem for List {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.items.len() * std::mem::size_of::<Value>()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Skip iteration if no refs - major GC optimization for lists of primitives\n        if !self.contains_refs {\n            return;\n        }\n        for obj in &mut self.items {\n            if let Value::Ref(id) = obj {\n                stack.push(*id);\n                #[cfg(feature = \"ref-count-panic\")]\n                obj.dec_ref_forget();\n            }\n        }\n    }\n}\n\n/// Dispatches a method call on a list value.\n///\n/// This is the unified entry point for list method calls.\n///\n/// # Arguments\n/// * `list` - The list to call the method on\n/// * `method` - The method to call (e.g., `StaticStrings::Append`)\n/// * `args` - The method arguments\n/// * `heap` - The heap for allocation and reference counting\nfn call_list_method(\n    list: &mut List,\n    method: StaticStrings,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Value> {\n    let heap = &mut *vm.heap;\n    match method {\n        StaticStrings::Append => {\n            let item = args.get_one_arg(\"list.append\", heap)?;\n            list.append(heap, item);\n            Ok(Value::None)\n        }\n        StaticStrings::Insert => list_insert(list, args, heap),\n        StaticStrings::Pop => list_pop(list, args, heap),\n        StaticStrings::Remove => list_remove(list, args, vm),\n        StaticStrings::Clear => {\n            args.check_zero_args(\"list.clear\", heap)?;\n            list_clear(list, heap);\n            Ok(Value::None)\n        }\n        StaticStrings::Copy => {\n            args.check_zero_args(\"list.copy\", heap)?;\n            Ok(list_copy(list, heap)?)\n        }\n        StaticStrings::Extend => list_extend(list, args, vm),\n        StaticStrings::Index => list_index(list, args, vm),\n        StaticStrings::Count => list_count(list, args, vm),\n        StaticStrings::Reverse => {\n            args.check_zero_args(\"list.reverse\", heap)?;\n            list.items.reverse();\n            Ok(Value::None)\n        }\n        // Note: list.sort is handled by py_call_attr which intercepts it before reaching here\n        _ => {\n            args.drop_with_heap(heap);\n            Err(ExcType::attribute_error(Type::List, method.into()))\n        }\n    }\n}\n\n/// Implements Python's `list.insert(index, item)` method.\nfn list_insert(list: &mut List, args: ArgValues, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    let (index_obj, item) = args.get_two_args(\"insert\", heap)?;\n    defer_drop!(index_obj, heap);\n    let mut item_guard = HeapGuard::new(item, heap);\n    let heap = item_guard.heap();\n    // Python's insert() handles negative indices by adding len\n    // If still negative after adding len, clamps to 0\n    // If >= len, appends to end\n    let index_i64 = index_obj.as_int(heap)?;\n    let len = list.items.len();\n    let len_i64 = i64::try_from(len).expect(\"list length exceeds i64::MAX\");\n    let index = if index_i64 < 0 {\n        // Negative index: add length, clamp to 0 if still negative\n        let adjusted = index_i64 + len_i64;\n        usize::try_from(adjusted).unwrap_or(0)\n    } else {\n        // Positive index: clamp to len if too large\n        usize::try_from(index_i64).unwrap_or(len)\n    };\n    let (item, heap) = item_guard.into_parts();\n    list.insert(heap, index, item);\n    Ok(Value::None)\n}\n\n/// Implements Python's `list.pop([index])` method.\n///\n/// Removes the item at the given index (default: -1) and returns it.\n/// Raises IndexError if the list is empty or the index is out of range.\nfn list_pop(list: &mut List, args: ArgValues, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    let index_arg = args.get_zero_one_arg(\"list.pop\", heap)?;\n\n    // Validate index type FIRST (if provided), matching Python's validation order.\n    // Python raises TypeError for bad index type even on empty list.\n    let index_i64 = if let Some(v) = index_arg {\n        let result = v.as_int(heap);\n        v.drop_with_heap(heap);\n        result?\n    } else {\n        -1\n    };\n\n    // THEN check empty list\n    if list.items.is_empty() {\n        return Err(ExcType::index_error_pop_empty_list());\n    }\n\n    // Normalize index\n    let len = list.items.len();\n    let len_i64 = i64::try_from(len).expect(\"list length exceeds i64::MAX\");\n    let normalized = if index_i64 < 0 { index_i64 + len_i64 } else { index_i64 };\n\n    // Bounds check\n    if normalized < 0 || normalized >= len_i64 {\n        return Err(ExcType::index_error_pop_out_of_range());\n    }\n\n    // Remove and return the item\n    let idx = usize::try_from(normalized).expect(\"index validated non-negative\");\n    Ok(list.items.remove(idx))\n}\n\n/// Implements Python's `list.remove(value)` method.\n///\n/// Removes the first occurrence of value. Raises ValueError if not found.\nfn list_remove(list: &mut List, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let value = args.get_one_arg(\"list.remove\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    // Find the first matching element\n    let mut found_idx = None;\n    for (i, item) in list.items.iter().enumerate() {\n        vm.heap.check_time()?;\n        if value.py_eq(item, vm)? {\n            found_idx = Some(i);\n            break;\n        }\n    }\n\n    match found_idx {\n        Some(idx) => {\n            // Remove the element and drop its refcount\n            let removed = list.items.remove(idx);\n            removed.drop_with_heap(vm.heap);\n            Ok(Value::None)\n        }\n        None => Err(ExcType::value_error_remove_not_in_list()),\n    }\n}\n\n/// Implements Python's `list.clear()` method.\n///\n/// Removes all items from the list.\nfn list_clear(list: &mut List, heap: &mut Heap<impl ResourceTracker>) {\n    list.items.drain(..).drop_with_heap(heap);\n    // Note: contains_refs stays true even if all refs removed, per conservative GC strategy\n}\n\n/// Implements Python's `list.copy()` method.\n///\n/// Returns a shallow copy of the list.\nfn list_copy(list: &List, heap: &mut Heap<impl ResourceTracker>) -> Result<Value, ResourceError> {\n    let items: Vec<Value> = list.items.iter().map(|v| v.clone_with_heap(heap)).collect();\n    let heap_id = heap.allocate(HeapData::List(List::new(items)))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Implements Python's `list.extend(iterable)` method.\n///\n/// Extends the list by appending all items from the iterable.\nfn list_extend(list: &mut List, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let iterable = args.get_one_arg(\"list.extend\", vm.heap)?;\n    let items: SmallVec<[_; 2]> = MontyIter::new(iterable, vm)?.collect(vm)?;\n\n    // Add each item to the list\n    for item in items {\n        list.append(vm.heap, item);\n    }\n\n    Ok(Value::None)\n}\n\n/// Implements Python's `list.index(value[, start[, end]])` method.\n///\n/// Returns the index of the first occurrence of value.\n/// Raises ValueError if the value is not found.\nfn list_index(list: &List, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let pos_args = args.into_pos_only(\"list.index\", vm.heap)?;\n    defer_drop!(pos_args, vm);\n\n    let len = list.items.len();\n    let (value, start, end) = match pos_args.as_slice() {\n        [] => return Err(ExcType::type_error_at_least(\"list.index\", 1, 0)),\n        [value] => (value, 0, len),\n        [value, start_arg] => {\n            let start = normalize_list_index(start_arg.as_int(vm.heap)?, len);\n            (value, start, len)\n        }\n        [value, start_arg, end_arg] => {\n            let start = normalize_list_index(start_arg.as_int(vm.heap)?, len);\n            let end = normalize_list_index(end_arg.as_int(vm.heap)?, len).max(start);\n            (value, start, end)\n        }\n        other => return Err(ExcType::type_error_at_most(\"list.index\", 3, other.len())),\n    };\n\n    // Search for the value in the specified range\n    for (i, item) in list.items[start..end].iter().enumerate() {\n        vm.heap.check_time()?;\n        if value.py_eq(item, vm)? {\n            let idx = i64::try_from(start + i).expect(\"index exceeds i64::MAX\");\n            return Ok(Value::Int(idx));\n        }\n    }\n\n    Err(ExcType::value_error_not_in_list())\n}\n\n/// Implements Python's `list.count(value)` method.\n///\n/// Returns the number of occurrences of value in the list.\nfn list_count(list: &List, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let value = args.get_one_arg(\"list.count\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    let mut count: usize = 0;\n    for item in &list.items {\n        vm.heap.check_time()?;\n        if value.py_eq(item, vm)? {\n            count += 1;\n        }\n    }\n\n    let count_i64 = i64::try_from(count).expect(\"count exceeds i64::MAX\");\n    Ok(Value::Int(count_i64))\n}\n\n/// Normalizes a Python-style list index to a valid index in range [0, len].\nfn normalize_list_index(index: i64, len: usize) -> usize {\n    if index < 0 {\n        let abs_index = usize::try_from(-index).unwrap_or(usize::MAX);\n        len.saturating_sub(abs_index)\n    } else {\n        usize::try_from(index).unwrap_or(len).min(len)\n    }\n}\n\n/// Performs an in-place sort on a list with optional key function and reverse flag.\nfn do_list_sort(list: &mut List, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<(), RunError> {\n    // Parse keyword-only arguments: key and reverse\n    let (key_arg, reverse_arg) = args.extract_keyword_only_pair(\"list.sort\", \"key\", \"reverse\", vm.heap, vm.interns)?;\n\n    // Convert reverse to bool (default false)\n    let reverse = if let Some(v) = reverse_arg {\n        let result = v.py_bool(vm);\n        v.drop_with_heap(vm);\n        result\n    } else {\n        false\n    };\n\n    // Handle key function (None means no key function)\n    let key_fn = match key_arg {\n        Some(v) if matches!(v, Value::None) => {\n            v.drop_with_heap(vm);\n            None\n        }\n        other => other,\n    };\n    defer_drop!(key_fn, vm);\n\n    // Step 1: Borrow from the list for in-place sorting\n    let items = list.as_vec_mut();\n\n    // 2. Compute key values if a key function was provided, otherwise we'll sort by the items themselves\n    let mut keys_guard;\n    let (compare_values, vm) = if let Some(f) = key_fn {\n        let keys: Vec<Value> = Vec::with_capacity(items.len());\n        // Use a HeapGuard to ensure that if key function evaluation fails partway through,\n        // we clean up any keys that were successfully computed\n        keys_guard = HeapGuard::new(keys, vm);\n        let (keys, vm) = keys_guard.as_parts_mut();\n        items\n            .iter()\n            .map(|item| {\n                let item = item.clone_with_heap(vm);\n                vm.evaluate_function(\"sorted() key argument\", f, ArgValues::One(item))\n            })\n            .process_results(|keys_iter| keys.extend(keys_iter))?;\n        keys_guard.as_parts()\n    } else {\n        (&*items, vm)\n    };\n\n    // 3. Sort indices by comparing key values (or items themselves if no key)\n    let len = compare_values.len();\n    let mut indices: Vec<usize> = (0..len).collect();\n\n    sort_indices(&mut indices, compare_values, reverse, vm)?;\n\n    // 4. Rearrange items in-place according to the sorted permutation\n    apply_permutation(items, &mut indices);\n    Ok(())\n}\n\n/// Writes a formatted sequence of values to a formatter.\n///\n/// This helper function is used to implement `__repr__` for sequence types like\n/// lists and tuples. It writes items as comma-separated repr interns.\n///\n/// # Arguments\n/// * `start` - The opening character (e.g., '[' for lists, '(' for tuples)\n/// * `end` - The closing character (e.g., ']' for lists, ')' for tuples)\n/// * `items` - The slice of values to format\n/// * `f` - The formatter to write to\n/// * `vm` - The VM for resolving value references and looking up interned strings\n/// * `heap_ids` - Set of heap IDs being repr'd (for cycle detection)\npub(crate) fn repr_sequence_fmt(\n    start: char,\n    end: char,\n    items: &[Value],\n    f: &mut impl Write,\n    vm: &VM<'_, '_, impl ResourceTracker>,\n    heap_ids: &mut AHashSet<HeapId>,\n) -> std::fmt::Result {\n    // Check depth limit before recursing\n    let heap = &*vm.heap;\n    let Some(token) = heap.incr_recursion_depth_for_repr() else {\n        return f.write_str(\"...\");\n    };\n    crate::defer_drop_immutable_heap!(token, heap);\n\n    f.write_char(start)?;\n    let mut iter = items.iter();\n    if let Some(first) = iter.next() {\n        first.py_repr_fmt(f, vm, heap_ids)?;\n        for item in iter {\n            if heap.check_time().is_err() {\n                f.write_str(\", ...[timeout]\")?;\n                break;\n            }\n            f.write_str(\", \")?;\n            item.py_repr_fmt(f, vm, heap_ids)?;\n        }\n    }\n    f.write_char(end)?;\n\n    Ok(())\n}\n\n/// Helper to extract items from a slice for list/tuple slicing.\n///\n/// Handles both positive and negative step values. For negative step,\n/// iterates backward from start down to (but not including) stop.\n///\n/// Returns a new Vec of cloned values with proper refcount increments.\n/// Checks the time limit on each iteration to enforce timeouts during slicing.\n///\n/// Note: step must be non-zero (callers should validate this via `slice.indices()`).\npub(crate) fn get_slice_items(\n    items: &[Value],\n    start: usize,\n    stop: usize,\n    step: i64,\n    heap: &mut Heap<impl ResourceTracker>,\n) -> RunResult<Vec<Value>> {\n    let mut result = Vec::new();\n\n    // try_from succeeds for non-negative step; step==0 rejected upstream by slice.indices()\n    if let Ok(step_usize) = usize::try_from(step) {\n        // Positive step: iterate forward\n        let mut i = start;\n        while i < stop && i < items.len() {\n            heap.check_time()?;\n            result.push(items[i].clone_with_heap(heap));\n            i += step_usize;\n        }\n    } else {\n        // Negative step: iterate backward\n        // start is the highest index, stop is the sentinel\n        // stop > items.len() means \"go to the beginning\"\n        let step_abs = usize::try_from(-step).expect(\"step is negative so -step is positive\");\n        let step_abs_i64 = i64::try_from(step_abs).expect(\"step magnitude fits in i64\");\n        let mut i = i64::try_from(start).expect(\"start index fits in i64\");\n        let stop_i64 = if stop > items.len() {\n            -1\n        } else {\n            i64::try_from(stop).expect(\"stop bounded by items.len() fits in i64\")\n        };\n\n        while let Ok(i_usize) = usize::try_from(i) {\n            if i_usize >= items.len() || i <= stop_i64 {\n                break;\n            }\n            heap.check_time()?;\n            result.push(items[i_usize].clone_with_heap(heap));\n            i -= step_abs_i64;\n        }\n    }\n\n    Ok(result)\n}\n\n#[cfg(test)]\nmod tests {\n    use num_bigint::BigInt;\n\n    use super::*;\n    use crate::{\n        PrintWriter,\n        intern::{InternerBuilder, Interns},\n        resource::NoLimitTracker,\n        types::LongInt,\n    };\n\n    /// Creates a minimal Interns for testing.\n    fn create_test_interns() -> Interns {\n        let interner = InternerBuilder::new(\"\");\n        Interns::new(interner, vec![])\n    }\n\n    /// Creates a heap with a list and a LongInt index, bypassing into_value() demotion.\n    ///\n    /// This allows testing the defensive code path where a LongInt contains an i64-fitting value.\n    fn create_heap_with_list_and_longint(\n        list_items: Vec<Value>,\n        index_value: BigInt,\n    ) -> (Heap<NoLimitTracker>, HeapId, HeapId) {\n        let mut heap = Heap::new(16, NoLimitTracker);\n        let list = List::new(list_items);\n        let list_id = heap.allocate(HeapData::List(list)).unwrap();\n        let long_int = LongInt::new(index_value);\n        let index_id = heap.allocate(HeapData::LongInt(long_int)).unwrap();\n        (heap, list_id, index_id)\n    }\n\n    /// Tests py_setitem with a LongInt index that fits in i64.\n    ///\n    /// This is a defensive code path - normally unreachable because LongInt::into_value()\n    /// demotes i64-fitting values to Value::Int. However, it could be reached via\n    /// deserialization of crafted snapshot data.\n    #[test]\n    fn py_setitem_longint_fits_in_i64() {\n        let (mut heap, list_id, index_id) =\n            create_heap_with_list_and_longint(vec![Value::Int(10), Value::Int(20), Value::Int(30)], BigInt::from(1));\n        let interns = create_test_interns();\n\n        // Use heap.with_entry_mut to avoid double mutable borrow\n        let key = Value::Ref(index_id);\n        let new_value = Value::Int(99);\n        heap.inc_ref(index_id);\n\n        let mut vm = VM::new(Vec::new(), &mut heap, &interns, PrintWriter::Disabled);\n        let result = Heap::with_entry_mut(&mut vm, list_id, |vm, mut data| data.py_setitem(key, new_value, vm));\n\n        assert!(result.is_ok());\n\n        // Verify the list was updated by checking it matches expected Int value\n        let HeapData::List(list) = heap.get(list_id) else {\n            panic!(\"expected list\");\n        };\n        assert!(matches!(list.as_slice()[1], Value::Int(99)));\n\n        // Clean up\n        Value::Ref(list_id).drop_with_heap(&mut heap);\n    }\n\n    /// Tests py_setitem with a negative LongInt index that fits in i64.\n    #[test]\n    fn py_setitem_longint_negative_fits_in_i64() {\n        let (mut heap, list_id, index_id) = create_heap_with_list_and_longint(\n            vec![Value::Int(10), Value::Int(20), Value::Int(30)],\n            BigInt::from(-1), // Last element\n        );\n        let interns = create_test_interns();\n\n        let key = Value::Ref(index_id);\n        let new_value = Value::Int(99);\n        heap.inc_ref(index_id);\n\n        let mut vm = VM::new(Vec::new(), &mut heap, &interns, PrintWriter::Disabled);\n        let result = Heap::with_entry_mut(&mut vm, list_id, |vm, mut data| data.py_setitem(key, new_value, vm));\n\n        assert!(result.is_ok());\n\n        // Verify the last element was updated\n        let HeapData::List(list) = heap.get(list_id) else {\n            panic!(\"expected list\");\n        };\n        assert!(matches!(list.as_slice()[2], Value::Int(99)));\n\n        Value::Ref(list_id).drop_with_heap(&mut heap);\n    }\n\n    /// Tests py_setitem with i64::MAX as a LongInt index.\n    #[test]\n    fn py_setitem_longint_at_i64_max() {\n        let (mut heap, list_id, index_id) =\n            create_heap_with_list_and_longint(vec![Value::Int(10)], BigInt::from(i64::MAX));\n        let interns = create_test_interns();\n\n        let key = Value::Ref(index_id);\n        let new_value = Value::Int(99);\n        heap.inc_ref(index_id);\n\n        // This should fail with IndexError because i64::MAX is out of bounds for a 1-element list\n        let mut vm = VM::new(Vec::new(), &mut heap, &interns, PrintWriter::Disabled);\n        let result = Heap::with_entry_mut(&mut vm, list_id, |vm, mut data| data.py_setitem(key, new_value, vm));\n\n        assert!(result.is_err());\n\n        Value::Ref(list_id).drop_with_heap(&mut heap);\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/long_int.rs",
    "content": "//! LongInt wrapper for arbitrary precision integer support.\n//!\n//! This module provides the `LongInt` wrapper type around `num_bigint::BigInt`.\n//! Named `LongInt` to avoid confusion with the external `BigInt` type. Python has\n//! one `int` type, and LongInt is an implementation detail - we use i64 for performance\n//! when values fit, and promote to LongInt on overflow.\n//!\n//! The design centralizes BigInt-related logic into methods on `LongInt` rather than\n//! having freestanding functions scattered across the codebase.\n\nuse std::{\n    collections::hash_map::DefaultHasher,\n    fmt::{self, Display},\n    hash::{Hash, Hasher},\n    ops::{Add, Mul, Neg, Sub},\n};\n\nuse num_bigint::BigInt;\nuse num_traits::{Signed, ToPrimitive, Zero};\n\nuse crate::{\n    heap::{Heap, HeapData},\n    resource::{ResourceError, ResourceTracker},\n    value::Value,\n};\n\n/// Wrapper around `num_bigint::BigInt` for arbitrary precision integers.\n///\n/// Named `LongInt` to avoid confusion with the external `BigInt` type from `num_bigint`.\n/// The inner `BigInt` is accessible via `.0` for arithmetic operations that need direct\n/// access to the underlying type.\n///\n/// Python treats all integers as one type - we use `Value::Int(i64)` for values that fit\n/// and `LongInt` for larger values. The `into_value()` method automatically demotes to\n/// i64 when the value fits, maintaining this optimization.\n#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]\npub struct LongInt(pub BigInt);\n\nimpl LongInt {\n    /// Creates a new `LongInt` from a `BigInt`.\n    pub fn new(bi: BigInt) -> Self {\n        Self(bi)\n    }\n\n    /// Converts to a `Value`, demoting to i64 if it fits.\n    ///\n    /// For performance, we want to keep values as `Value::Int(i64)` whenever possible.\n    /// This method checks if the value fits in an i64 and returns `Value::Int` if so,\n    /// otherwise allocates a `HeapData::LongInt` on the heap.\n    pub fn into_value(self, heap: &mut Heap<impl ResourceTracker>) -> Result<Value, ResourceError> {\n        // Try to demote back to i64 for performance\n        if let Some(i) = self.0.to_i64() {\n            Ok(Value::Int(i))\n        } else {\n            let heap_id = heap.allocate(HeapData::LongInt(self))?;\n            Ok(Value::Ref(heap_id))\n        }\n    }\n\n    /// Computes a hash consistent with i64 hashing.\n    ///\n    /// Critical: For values that fit in i64, this must return the same hash as\n    /// hashing the i64 directly. This ensures dict key consistency - e.g.,\n    /// `hash(5)` must equal `hash(LongInt(5))`.\n    pub fn hash(&self) -> u64 {\n        // If the LongInt fits in i64, hash as i64 for consistency\n        if let Some(i) = self.0.to_i64() {\n            let mut hasher = DefaultHasher::new();\n            // Hash the i64 discriminant and value to match Value::Int hashing\n            std::mem::discriminant(&Value::Int(0)).hash(&mut hasher);\n            i.hash(&mut hasher);\n            hasher.finish()\n        } else {\n            // For LongInts outside i64 range, use byte representation\n            let mut hasher = DefaultHasher::new();\n            // Use a unique discriminant for LongInt (we use the LongInt's sign and bytes)\n            let (sign, bytes) = self.0.to_bytes_le();\n            sign.hash(&mut hasher);\n            bytes.hash(&mut hasher);\n            hasher.finish()\n        }\n    }\n\n    /// Estimates memory size in bytes.\n    ///\n    /// Used for resource tracking. The actual size includes the Vec overhead\n    /// plus the digit storage. Rounds up bits to bytes to avoid underestimating\n    /// (e.g., 1 bit = 1 byte, not 0 bytes).\n    pub fn estimate_size(&self) -> usize {\n        // Each BigInt digit is typically a u32 or u64\n        // We estimate based on the number of significant bits\n        let bits = self.0.bits();\n        // Convert bits to bytes (round up), add overhead for Vec and sign\n        // On 32-bit platforms, truncate to usize::MAX if bits is too large\n        let bit_bytes = usize::try_from(bits).unwrap_or(usize::MAX).saturating_add(7) / 8;\n        bit_bytes + std::mem::size_of::<BigInt>()\n    }\n\n    /// Returns a reference to the inner `BigInt`.\n    ///\n    /// Use this when you need read-only access to the underlying `BigInt`\n    /// for operations like formatting or comparison.\n    pub fn inner(&self) -> &BigInt {\n        &self.0\n    }\n\n    /// Checks if the value is zero.\n    pub fn is_zero(&self) -> bool {\n        self.0.is_zero()\n    }\n\n    /// Checks if the value is negative.\n    pub fn is_negative(&self) -> bool {\n        self.0.is_negative()\n    }\n\n    /// Tries to convert to i64.\n    ///\n    /// Returns `Some(i64)` if the value fits, `None` otherwise.\n    pub fn to_i64(&self) -> Option<i64> {\n        self.0.to_i64()\n    }\n\n    /// Tries to convert to f64.\n    ///\n    /// Returns `Some(f64)` if the conversion is possible, `None` if the value\n    /// is too large to represent as f64.\n    pub fn to_f64(&self) -> Option<f64> {\n        self.0.to_f64()\n    }\n\n    /// Tries to convert to u32.\n    ///\n    /// Returns `Some(u32)` if the value fits, `None` otherwise.\n    pub fn to_u32(&self) -> Option<u32> {\n        self.0.to_u32()\n    }\n\n    /// Tries to convert to usize.\n    ///\n    /// Returns `Some(usize)` if the value fits, `None` otherwise.\n    /// Useful for sequence repetition counts.\n    pub fn to_usize(&self) -> Option<usize> {\n        self.0.to_usize()\n    }\n\n    /// Returns the absolute value as a new `LongInt`.\n    pub fn abs(&self) -> Self {\n        Self(self.0.abs())\n    }\n\n    /// Returns the number of significant bits in this LongInt.\n    ///\n    /// Zero returns 0 bits. For non-zero values, this is the position of the\n    /// highest set bit plus one.\n    pub fn bits(&self) -> u64 {\n        self.0.bits()\n    }\n}\n\n// === Trait Implementations ===\n\nimpl From<BigInt> for LongInt {\n    fn from(bi: BigInt) -> Self {\n        Self(bi)\n    }\n}\n\nimpl From<i64> for LongInt {\n    fn from(i: i64) -> Self {\n        Self(BigInt::from(i))\n    }\n}\n\nimpl Add for LongInt {\n    type Output = Self;\n\n    fn add(self, rhs: Self) -> Self::Output {\n        Self(self.0 + rhs.0)\n    }\n}\n\nimpl Sub for LongInt {\n    type Output = Self;\n\n    fn sub(self, rhs: Self) -> Self::Output {\n        Self(self.0 - rhs.0)\n    }\n}\n\nimpl Mul for LongInt {\n    type Output = Self;\n\n    fn mul(self, rhs: Self) -> Self::Output {\n        Self(self.0 * rhs.0)\n    }\n}\n\nimpl Neg for LongInt {\n    type Output = Self;\n\n    fn neg(self) -> Self::Output {\n        Self(-self.0)\n    }\n}\n\nimpl Display for LongInt {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/mod.rs",
    "content": "/// Type definitions for Python runtime values.\n///\n/// This module contains structured types that wrap heap-allocated data\n/// and provide Python-like semantics for operations like append, insert, etc.\n///\n/// The `AbstractValue` trait provides a common interface for all heap-allocated\n/// types, enabling efficient dispatch via `enum_dispatch`.\npub mod bytes;\npub mod dataclass;\npub mod dict;\npub mod dict_view;\npub mod iter;\npub mod list;\npub mod long_int;\npub mod module;\npub mod namedtuple;\npub mod path;\npub mod property;\npub mod py_trait;\npub mod range;\npub mod re_match;\npub mod re_pattern;\npub mod set;\npub mod slice;\npub mod str;\npub mod tuple;\npub mod r#type;\n\npub(crate) use bytes::Bytes;\npub(crate) use dataclass::Dataclass;\npub(crate) use dict::Dict;\npub(crate) use dict_view::{DictItemsView, DictKeysView, DictValuesView};\npub(crate) use iter::MontyIter;\npub(crate) use list::List;\npub(crate) use long_int::LongInt;\npub(crate) use module::Module;\npub(crate) use namedtuple::NamedTuple;\npub(crate) use path::Path;\npub(crate) use property::Property;\npub(crate) use py_trait::PyTrait;\npub(crate) use range::Range;\npub(crate) use re_match::ReMatch;\npub(crate) use re_pattern::RePattern;\npub(crate) use set::{FrozenSet, Set};\npub(crate) use slice::Slice;\npub(crate) use str::Str;\npub(crate) use tuple::{Tuple, allocate_tuple};\npub(crate) use r#type::Type;\n"
  },
  {
    "path": "crates/monty/src/types/module.rs",
    "content": "//! Python module type for representing imported modules.\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapGuard, HeapId, HeapItem},\n    intern::{Interns, StringId},\n    resource::ResourceTracker,\n    types::Dict,\n    value::{EitherStr, Value},\n};\n\n/// A Python module with a name and attribute dictionary.\n///\n/// Modules in Monty are simplified compared to CPython - they just have a name\n/// and a dictionary of attributes. This is sufficient for built-in modules like\n/// `sys` and `typing` where we control the available attributes.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct Module {\n    /// The module name (e.g., \"sys\", \"typing\").\n    name: StringId,\n    /// The module's attributes (e.g., `version`, `platform` for `sys`).\n    attrs: Dict,\n}\n\nimpl Module {\n    /// Creates a new module with an empty attributes dictionary.\n    ///\n    /// The module name must be pre-interned during the prepare phase.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the module name string has not been pre-interned.\n    pub fn new(name: impl Into<StringId>) -> Self {\n        Self {\n            name: name.into(),\n            attrs: Dict::new(),\n        }\n    }\n\n    /// Returns the module's name StringId.\n    pub fn name(&self) -> StringId {\n        self.name\n    }\n\n    /// Returns a reference to the module's attribute dictionary.\n    pub fn attrs(&self) -> &Dict {\n        &self.attrs\n    }\n\n    /// Sets an attribute in the module's dictionary.\n    ///\n    /// The attribute name must be pre-interned during the prepare phase.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the attribute name string has not been pre-interned.\n    pub fn set_attr(&mut self, name: impl Into<StringId>, value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) {\n        let key = Value::InternString(name.into());\n        // Unwrap is safe because InternString keys are always hashable\n        self.attrs.set(key, value, vm).unwrap();\n    }\n\n    /// Looks up an attribute by name in the module's attribute dictionary.\n    ///\n    /// Returns `Some(value)` if the attribute exists, `None` otherwise.\n    /// The returned value is cloned with proper refcount handling.\n    pub fn get_attr(&self, attr_value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Option<Value> {\n        // Dict::get returns Result because of hash computation, but InternString keys\n        // are always hashable, so `.ok()` is safe here.\n        self.attrs\n            .get(attr_value, vm)\n            .ok()\n            .flatten()\n            .map(|v| v.clone_with_heap(vm))\n    }\n\n    /// Returns whether this module has any heap references in its attributes.\n    pub fn has_refs(&self) -> bool {\n        self.attrs.has_refs()\n    }\n\n    /// Collects child HeapIds for reference counting.\n    pub fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.attrs.py_dec_ref_ids(stack);\n    }\n\n    /// Gets an attribute by string ID for the `py_getattr` trait method.\n    ///\n    /// Returns the attribute value if found, or `None` if the attribute doesn't exist.\n    /// For `Property` values, invokes the property getter rather than returning\n    /// the Property itself - this implements Python's descriptor protocol.\n    pub fn py_getattr(\n        &self,\n        attr: &EitherStr,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> Option<CallResult> {\n        let value = self.attrs.get_by_str(attr.as_str(interns), heap, interns)?;\n\n        // If the value is a Property, invoke its getter to compute the actual value\n        if let Value::Property(prop) = *value {\n            Some(prop.get())\n        } else {\n            Some(CallResult::Value(value.clone_with_heap(heap)))\n        }\n    }\n\n    /// Calls an attribute as a function on this module.\n    ///\n    /// Modules don't have methods - they have callable attributes. This looks up\n    /// the attribute and calls it if it's a `ModuleFunction`.\n    ///\n    /// Returns `CallResult` because module functions may need OS operations\n    /// (e.g., `os.getenv()`) that require host involvement.\n    pub fn py_call_attr(\n        &self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let mut args_guard = HeapGuard::new(args, vm);\n        let vm = args_guard.heap();\n        let attr_key = match attr {\n            EitherStr::Interned(id) => Value::InternString(*id),\n            EitherStr::Heap(s) => {\n                return Err(ExcType::attribute_error_module(vm.interns.get_str(self.name), s));\n            }\n        };\n\n        match self.get_attr(&attr_key, vm) {\n            Some(value) => {\n                let (args, vm) = args_guard.into_parts();\n                defer_drop!(value, vm);\n                vm.call_function(value, args)\n            }\n            None => Err(ExcType::attribute_error_module(\n                vm.interns.get_str(self.name),\n                attr.as_str(vm.interns),\n            )),\n        }\n    }\n}\n\nimpl HeapItem for Module {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.attrs.py_estimate_size()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.attrs.py_dec_ref_ids(stack);\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/namedtuple.rs",
    "content": "/// Python named tuple type, combining tuple-like indexing with named attribute access.\n///\n/// Named tuples are like regular tuples but with field names, providing two ways\n/// to access elements:\n/// - By index: `version_info[0]` returns the major version\n/// - By name: `version_info.major` returns the same value\n///\n/// Named tuples are:\n/// - Immutable (all tuple semantics apply)\n/// - Hashable (if all elements are hashable)\n/// - Have a descriptive repr: `sys.version_info(major=3, minor=14, ...)`\n/// - Support `len()` and iteration\n///\n/// # Use Case\n///\n/// This type is used for `sys.version_info` and similar structured tuples where\n/// named access improves usability and readability.\nuse std::fmt::Write;\n\nuse ahash::AHashSet;\n\nuse super::PyTrait;\nuse crate::{\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapId, HeapItem},\n    intern::{Interns, StringId},\n    resource::{ResourceError, ResourceTracker},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Python named tuple value stored on the heap.\n///\n/// Wraps a `Vec<Value>` with associated field names and provides both index-based\n/// and name-based access. Named tuples are conceptually immutable, though this is\n/// not enforced at the type level for internal operations.\n///\n/// # Reference Counting\n///\n/// When a named tuple is freed, all contained heap references have their refcounts\n/// decremented via `py_dec_ref_ids`.\n///\n/// # GC Optimization\n///\n/// The `contains_refs` flag tracks whether the tuple contains any `Value::Ref` items.\n/// This allows `py_dec_ref_ids` to skip iteration when the tuple contains only\n/// primitive values (ints, bools, None, etc.).\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) struct NamedTuple {\n    /// Type name for repr (e.g., \"sys.version_info\").\n    name: EitherStr,\n    /// Field names in order, e.g., `major`, `minor`, `micro`, `releaselevel`, `serial`.\n    field_names: Vec<EitherStr>,\n    /// Values in order (same length as field_names).\n    items: Vec<Value>,\n    /// True if any item is a `Value::Ref`. Set at creation time since named tuples are immutable.\n    contains_refs: bool,\n}\n\nimpl NamedTuple {\n    /// Creates a new named tuple.\n    ///\n    /// # Arguments\n    ///\n    /// * `type_name` - The type name for repr (e.g., \"sys.version_info\")\n    /// * `field_names` - Field names as interned StringIds, in order\n    /// * `items` - Values corresponding to each field name\n    ///\n    /// # Panics\n    ///\n    /// Panics if `field_names.len() != items.len()`.\n    #[must_use]\n    pub fn new(name: impl Into<EitherStr>, field_names: Vec<EitherStr>, items: Vec<Value>) -> Self {\n        assert_eq!(\n            field_names.len(),\n            items.len(),\n            \"NamedTuple field_names and items must have same length\"\n        );\n        let contains_refs = items.iter().any(|v| matches!(v, Value::Ref(_)));\n        Self {\n            name: name.into(),\n            field_names,\n            items,\n            contains_refs,\n        }\n    }\n\n    /// Returns the type name (e.g., \"sys.version_info\").\n    #[must_use]\n    pub fn name<'a>(&'a self, interns: &'a Interns) -> &'a str {\n        self.name.as_str(interns)\n    }\n\n    /// Returns a reference to the field names.\n    #[must_use]\n    pub fn field_names(&self) -> &[EitherStr] {\n        &self.field_names\n    }\n\n    /// Returns a reference to the underlying items vector.\n    #[must_use]\n    pub fn as_vec(&self) -> &Vec<Value> {\n        &self.items\n    }\n\n    /// Returns the number of elements.\n    #[must_use]\n    pub fn len(&self) -> usize {\n        self.items.len()\n    }\n\n    /// Returns whether the tuple contains any heap references.\n    ///\n    /// When false, `py_dec_ref_ids` can skip iteration.\n    #[inline]\n    #[must_use]\n    pub fn contains_refs(&self) -> bool {\n        self.contains_refs\n    }\n\n    /// Gets a field value by name.\n    ///\n    /// Compares field names by actual string content, not just variant type.\n    /// This allows lookup to work regardless of whether the field name was\n    /// stored as an interned `StringId` or a heap-allocated `String`.\n    ///\n    /// Returns `Some(value)` if the field exists, `None` otherwise.\n    #[must_use]\n    pub fn get_by_name(&self, name_str: &str, interns: &Interns) -> Option<&Value> {\n        self.field_names\n            .iter()\n            .position(|field_name| field_name.as_str(interns) == name_str)\n            .map(|idx| &self.items[idx])\n    }\n\n    /// Gets a field value by index, supporting negative indexing.\n    ///\n    /// Returns `Some(value)` if the index is in bounds, `None` otherwise.\n    /// Uses `index + len` instead of `-index` to avoid overflow on `i64::MIN`.\n    #[must_use]\n    pub fn get_by_index(&self, index: i64) -> Option<&Value> {\n        let len = i64::try_from(self.items.len()).ok()?;\n        let normalized = if index < 0 { index + len } else { index };\n        if normalized < 0 || normalized >= len {\n            return None;\n        }\n        self.items.get(usize::try_from(normalized).ok()?)\n    }\n}\n\nimpl PyTrait for NamedTuple {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::NamedTuple\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.items.len())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        // Extract integer index from key, returning TypeError if not an int\n        let index = match key {\n            Value::Int(i) => *i,\n            _ => return Err(ExcType::type_error_indices(Type::NamedTuple, key.py_type(vm.heap))),\n        };\n\n        // Get by index with bounds checking\n        match self.get_by_index(index) {\n            Some(value) => Ok(value.clone_with_heap(vm.heap)),\n            None => Err(ExcType::tuple_index_error()),\n        }\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        // Compare only by items (not type_name) to match tuple semantics\n        // This allows sys.version_info == (3, 14, 0, 'final', 0) to work\n        if self.items.len() != other.items.len() {\n            return Ok(false);\n        }\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        for (i1, i2) in self.items.iter().zip(&other.items) {\n            if !i1.py_eq(i2, vm)? {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.items.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        // Check depth limit before recursing\n        let heap = &*vm.heap;\n        let Some(token) = heap.incr_recursion_depth_for_repr() else {\n            return f.write_str(\"...\");\n        };\n        crate::defer_drop_immutable_heap!(token, heap);\n\n        // Format: type_name(field1=value1, field2=value2, ...)\n        write!(f, \"{}(\", self.name.as_str(vm.interns))?;\n\n        let mut first = true;\n        for (field_name, value) in self.field_names.iter().zip(&self.items) {\n            if !first {\n                f.write_str(\", \")?;\n            }\n            first = false;\n            f.write_str(field_name.as_str(vm.interns))?;\n            f.write_char('=')?;\n            value.py_repr_fmt(f, vm, heap_ids)?;\n        }\n\n        f.write_char(')')?;\n        Ok(())\n    }\n\n    fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<CallResult>> {\n        let attr_name = attr.as_str(vm.interns);\n        if let Some(value) = self.get_by_name(attr_name, vm.interns) {\n            Ok(Some(CallResult::Value(value.clone_with_heap(vm.heap))))\n        } else {\n            // we use name here, not `self.py_type(heap)` hence returning a Ok(None)\n            Err(ExcType::attribute_error(self.name(vm.interns), attr_name))\n        }\n    }\n}\n\nimpl HeapItem for NamedTuple {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n            + self.name.py_estimate_size()\n            + self.field_names.len() * std::mem::size_of::<StringId>()\n            + self.items.len() * std::mem::size_of::<Value>()\n    }\n\n    /// Pushes all heap IDs contained in this named tuple onto the stack.\n    ///\n    /// Called during garbage collection to decrement refcounts of nested values.\n    /// When `ref-count-panic` is enabled, also marks all Values as Dereferenced.\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Skip iteration if no refs - GC optimization for tuples of primitives\n        if !self.contains_refs {\n            return;\n        }\n        for obj in &mut self.items {\n            if let Value::Ref(id) = obj {\n                stack.push(*id);\n                #[cfg(feature = \"ref-count-panic\")]\n                obj.dec_ref_forget();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/path.rs",
    "content": "//! Python `pathlib.Path` type implementation.\n//!\n//! Provides a path object with both pure methods (no I/O) and filesystem methods\n//! (require `OsAccess` implementation). Pure methods are handled directly by the VM,\n//! while filesystem methods yield external function calls for the host to resolve.\n\nuse std::fmt::Write;\n\nuse ahash::AHashSet;\nuse smallvec::SmallVec;\n\nuse crate::{\n    args::{ArgValues, KwargsValues},\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{DropWithHeap, Heap, HeapData, HeapId, HeapItem},\n    intern::{Interns, StaticStrings},\n    os::OsFunction,\n    resource::{ResourceError, ResourceTracker},\n    types::{PyTrait, Str, Type, allocate_tuple},\n    value::{EitherStr, Value},\n};\n\n/// Python `pathlib.Path` object representing a filesystem path.\n///\n/// Stores a normalized POSIX path string. Windows-style paths are converted\n/// to POSIX format (backslashes to forward slashes).\n///\n/// The path is immutable - all operations that would modify the path return\n/// new `Path` objects or strings.\n#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct Path {\n    /// The normalized path string.\n    path: String,\n}\n\nimpl Path {\n    /// Creates a new `Path` from a path string.\n    ///\n    /// The path is normalized:\n    /// - Backslashes are converted to forward slashes\n    /// - Trailing slashes are preserved for root paths only\n    #[must_use]\n    pub fn new(path: String) -> Self {\n        Self {\n            path: normalize_path(path),\n        }\n    }\n\n    /// Returns the path as a string slice.\n    #[must_use]\n    pub fn as_str(&self) -> &str {\n        &self.path\n    }\n\n    /// Returns the final component of the path.\n    ///\n    /// Returns an empty string if the path ends with a separator or is empty.\n    #[must_use]\n    pub fn name(&self) -> &str {\n        self.path.rsplit_once('/').map_or(self.path.as_str(), |(_, name)| name)\n    }\n\n    /// Returns the path without its final component (parent directory).\n    ///\n    /// For relative paths without a directory (like `file.txt`), returns `.`.\n    /// Returns `None` only for the root path `/`.\n    #[must_use]\n    pub fn parent(&self) -> Option<&str> {\n        if self.path == \"/\" {\n            return None;\n        }\n        match self.path.rsplit_once('/') {\n            Some((parent, _)) => Some(if parent.is_empty() { \"/\" } else { parent }),\n            None => Some(\".\"), // Relative path without directory component\n        }\n    }\n\n    /// Returns the final component without its last suffix.\n    ///\n    /// If the name has multiple suffixes (e.g., \"file.tar.gz\"), only the\n    /// last suffix is removed.\n    #[must_use]\n    pub fn stem(&self) -> &str {\n        let name = self.name();\n        if name.starts_with('.') && !name[1..].contains('.') {\n            // Hidden file without extension (e.g., \".bashrc\")\n            return name;\n        }\n        name.rsplit_once('.').map_or(name, |(stem, _)| stem)\n    }\n\n    /// Returns the file extension (last suffix), including the leading dot.\n    ///\n    /// Returns an empty string if there is no extension.\n    #[must_use]\n    pub fn suffix(&self) -> &str {\n        let name = self.name();\n        if name.starts_with('.') && !name[1..].contains('.') {\n            // Hidden file without extension (e.g., \".bashrc\")\n            return \"\";\n        }\n        name.rfind('.').map_or(\"\", |idx| &name[idx..])\n    }\n\n    /// Returns all file extensions as a list of strings.\n    ///\n    /// Each suffix includes its leading dot. Returns an empty list if no extensions.\n    #[must_use]\n    pub fn suffixes(&self) -> Vec<&str> {\n        let name = self.name();\n        if name.is_empty() || name == \".\" || name == \"..\" {\n            return Vec::new();\n        }\n\n        let start_idx = usize::from(name.starts_with('.'));\n        let search_str = &name[start_idx..];\n\n        let mut result = Vec::new();\n        let mut pos = 0;\n        while let Some(idx) = search_str[pos..].find('.') {\n            let abs_idx = pos + idx;\n            // Each suffix is from this dot to the end or next dot\n            let suffix_end = search_str[abs_idx + 1..]\n                .find('.')\n                .map_or(search_str.len(), |next| abs_idx + 1 + next);\n            result.push(&name[start_idx + abs_idx..start_idx + suffix_end]);\n            pos = abs_idx + 1;\n        }\n        result\n    }\n\n    /// Returns the path components as a list of strings.\n    ///\n    /// Absolute paths start with \"/\" as the first component.\n    #[must_use]\n    pub fn parts(&self) -> Vec<&str> {\n        if self.path.is_empty() {\n            return Vec::new();\n        }\n\n        let mut parts = Vec::new();\n        if self.path.starts_with('/') {\n            parts.push(\"/\");\n            let rest = &self.path[1..];\n            if !rest.is_empty() {\n                parts.extend(rest.split('/').filter(|s| !s.is_empty()));\n            }\n        } else {\n            parts.extend(self.path.split('/').filter(|s| !s.is_empty()));\n        }\n        parts\n    }\n\n    /// Returns `true` if the path is absolute (starts with `/`).\n    #[must_use]\n    pub fn is_absolute(&self) -> bool {\n        self.path.starts_with('/')\n    }\n\n    /// Joins this path with another path component.\n    ///\n    /// If `other` is an absolute path, it replaces `self` entirely.\n    #[must_use]\n    pub fn joinpath(&self, other: &str) -> String {\n        if other.starts_with('/') || self.path.is_empty() || self.path == \".\" {\n            normalize_path(other.to_owned())\n        } else if self.path.ends_with('/') {\n            normalize_path(format!(\"{}{}\", self.path, other))\n        } else {\n            normalize_path(format!(\"{}/{}\", self.path, other))\n        }\n    }\n\n    /// Returns a new path with the name changed.\n    ///\n    /// # Errors\n    /// Returns an error if the path has no name or if the new name is empty.\n    pub fn with_name(&self, name: &str) -> Result<String, String> {\n        if name.is_empty() {\n            return Err(\"Invalid name: empty string\".to_owned());\n        }\n        if name.contains('/') {\n            return Err(format!(\"Invalid name: {name:?} contains path separator\"));\n        }\n        if self.name().is_empty() {\n            return Err(\"Path has no name\".to_owned());\n        }\n\n        if let Some(parent) = self.parent() {\n            if parent == \"/\" {\n                Ok(format!(\"/{name}\"))\n            } else if parent == \".\" {\n                // Relative path without directory - just use the new name\n                Ok(name.to_owned())\n            } else {\n                Ok(format!(\"{parent}/{name}\"))\n            }\n        } else {\n            Ok(name.to_owned())\n        }\n    }\n\n    /// Returns a new path with the stem changed (keeps the suffix).\n    ///\n    /// # Errors\n    /// Returns an error if the path has no name or if the new stem is empty.\n    pub fn with_stem(&self, stem: &str) -> Result<String, String> {\n        if stem.is_empty() {\n            return Err(\"Invalid stem: empty string\".to_owned());\n        }\n        if stem.contains('/') {\n            return Err(format!(\"Invalid stem: {stem:?} contains path separator\"));\n        }\n        if self.name().is_empty() {\n            return Err(\"Path has no name\".to_owned());\n        }\n\n        let suffix = self.suffix();\n        let new_name = format!(\"{stem}{suffix}\");\n        self.with_name(&new_name)\n    }\n\n    /// Returns a new path with the suffix changed.\n    ///\n    /// If the suffix is empty, removes the existing suffix.\n    /// If the suffix doesn't start with '.', it's added.\n    pub fn with_suffix(&self, suffix: &str) -> Result<String, String> {\n        if self.name().is_empty() {\n            return Err(\"Path has no name\".to_owned());\n        }\n\n        let suffix = if suffix.is_empty() || suffix.starts_with('.') {\n            suffix.to_owned()\n        } else {\n            format!(\".{suffix}\")\n        };\n\n        if suffix.contains('/') {\n            return Err(format!(\"Invalid suffix: {suffix:?} contains path separator\"));\n        }\n\n        let stem = self.stem();\n        let new_name = format!(\"{stem}{suffix}\");\n        self.with_name(&new_name)\n    }\n\n    /// Returns the path as a POSIX string (forward slashes).\n    ///\n    /// Since paths are already stored in POSIX format, this just returns the path.\n    #[must_use]\n    pub fn as_posix(&self) -> &str {\n        &self.path\n    }\n\n    /// Creates a `Path` from the `Path()` constructor call.\n    ///\n    /// Accepts zero or more path segments that are joined together.\n    /// - `Path()` returns `Path('.')`\n    /// - `Path('a')` returns `Path('a')`\n    /// - `Path('a', 'b', 'c')` returns `Path('a/b/c')`\n    /// - If an absolute path appears, it replaces everything before it.\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        let interns = vm.interns;\n        let pos_args = args.into_pos_only(\"Path\", heap)?;\n        defer_drop!(pos_args, heap);\n\n        let path = match pos_args.as_slice() {\n            [] => {\n                // No arguments, return Path('.')\n                Self::new(\".\".to_owned())\n            }\n            [single] => {\n                // Single argument, just convert to Path\n                Self::new(extract_path_string(single, heap, interns)?.to_owned())\n            }\n            [first_arg, rest @ ..] => {\n                let base = Self::new(extract_path_string(first_arg, heap, interns)?.to_owned());\n                fold_joinpath(base, rest, heap, interns)?\n            }\n        };\n        Ok(Value::Ref(heap.allocate(HeapData::Path(path))?))\n    }\n}\n\n/// Extracts a string from a Value for use as a path.\nfn extract_path_string<'a>(\n    val: &Value,\n    heap: &'a Heap<impl ResourceTracker>,\n    interns: &'a Interns,\n) -> RunResult<&'a str> {\n    match val {\n        Value::InternString(string_id) => Ok(interns.get_str(*string_id)),\n        Value::Ref(heap_id) => match heap.get(*heap_id) {\n            HeapData::Str(s) => Ok(s.as_str()),\n            HeapData::Path(p) => Ok(p.as_str()),\n            _ => Err(ExcType::type_error(format!(\n                \"expected str or Path, got {}\",\n                val.py_type(heap)\n            ))),\n        },\n        _ => Err(ExcType::type_error(format!(\n            \"expected str or Path, got {}\",\n            val.py_type(heap)\n        ))),\n    }\n}\n\nfn fold_joinpath(\n    mut path: Path,\n    parts: &[Value],\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Path> {\n    for part in parts {\n        path = Path::new(path.joinpath(extract_path_string(part, heap, interns)?));\n    }\n    Ok(path)\n}\n\n/// Handles the `/` operator for Path objects (path concatenation).\n///\n/// In Python, `Path('/usr') / 'bin'` produces `Path('/usr/bin')`.\npub(crate) fn path_div(\n    path_id: HeapId,\n    other: &Value,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Option<Value>> {\n    // Extract the right-hand side as a string\n    let other_str = match other {\n        Value::InternString(string_id) => interns.get_str(*string_id).to_owned(),\n        Value::Ref(other_id) => match heap.get(*other_id) {\n            HeapData::Str(s) => s.as_str().to_owned(),\n            HeapData::Path(p) => p.as_str().to_owned(),\n            _ => return Ok(None),\n        },\n        _ => return Ok(None),\n    };\n\n    // Get the path string\n    let path_str = match heap.get(path_id) {\n        HeapData::Path(p) => p.as_str().to_owned(),\n        _ => return Ok(None),\n    };\n\n    // Perform path concatenation\n    let result = Path::new(path_str).joinpath(&other_str);\n    Ok(Some(Value::Ref(heap.allocate(HeapData::Path(Path::new(result)))?)))\n}\n\n/// Normalizes a path string to POSIX format.\n///\n/// - Converts backslashes to forward slashes\n/// - Removes trailing slashes (except for root \"/\")\n/// - Does NOT resolve `.` or `..` components (that requires I/O for symlinks)\nfn normalize_path(mut path: String) -> String {\n    // Convert backslashes to forward slashes\n    if path.contains('\\\\') {\n        path = path.replace('\\\\', \"/\");\n    }\n\n    // Remove trailing slashes, but keep root \"/\"\n    while path.len() > 1 && path.ends_with('/') {\n        path.pop();\n    }\n\n    path\n}\n\n/// Prepends the path string argument to existing arguments for OS calls.\n///\n/// OS functions expect the path as the first argument, so we need to\n/// combine it with any additional arguments passed to the method.\nfn prepend_path_arg(path_arg: Value, args: ArgValues) -> ArgValues {\n    match args {\n        ArgValues::Empty => ArgValues::One(path_arg),\n        ArgValues::One(v) => ArgValues::Two(path_arg, v),\n        ArgValues::Two(a, b) => ArgValues::ArgsKargs {\n            args: vec![path_arg, a, b],\n            kwargs: KwargsValues::Empty,\n        },\n        ArgValues::Kwargs(kwargs) => ArgValues::ArgsKargs {\n            args: vec![path_arg],\n            kwargs,\n        },\n        ArgValues::ArgsKargs { args: mut vals, kwargs } => {\n            vals.insert(0, path_arg);\n            ArgValues::ArgsKargs { args: vals, kwargs }\n        }\n    }\n}\n\nimpl Path {\n    /// Resolves a known attribute by its `StaticStrings` variant.\n    ///\n    /// Returns `Ok(Some(value))` for recognized property names (`name`, `parent`,\n    /// `stem`, `suffix`, `suffixes`, `parts`), or `Ok(None)` if the variant doesn't\n    /// correspond to a Path attribute. Used by `py_getattr` to share logic between\n    /// the interned fast path and the heap string slow path.\n    fn getattr_by_static(&self, ss: StaticStrings, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Option<Value>> {\n        let v = match ss {\n            StaticStrings::Name => {\n                let name = self.name();\n                Value::Ref(heap.allocate(HeapData::Str(Str::new(name.to_owned())))?)\n            }\n            StaticStrings::Parent => {\n                if let Some(parent) = self.parent() {\n                    let parent_path = Self::new(parent.to_owned());\n                    Value::Ref(heap.allocate(HeapData::Path(parent_path))?)\n                } else {\n                    // Return self when there's no parent (root or relative path)\n                    let same_path = Self::new(self.as_str().to_owned());\n                    Value::Ref(heap.allocate(HeapData::Path(same_path))?)\n                }\n            }\n            StaticStrings::Stem => {\n                let stem = self.stem();\n                Value::Ref(heap.allocate(HeapData::Str(Str::new(stem.to_owned())))?)\n            }\n            StaticStrings::Suffix => {\n                let suffix = self.suffix();\n                Value::Ref(heap.allocate(HeapData::Str(Str::new(suffix.to_owned())))?)\n            }\n            StaticStrings::Suffixes => {\n                use crate::types::List;\n\n                let suffixes = self.suffixes();\n                let mut items = Vec::with_capacity(suffixes.len());\n                for suffix in suffixes {\n                    let str_id = heap.allocate(HeapData::Str(Str::new(suffix.to_owned())))?;\n                    items.push(Value::Ref(str_id));\n                }\n                Value::Ref(heap.allocate(HeapData::List(List::new(items)))?)\n            }\n            StaticStrings::Parts => {\n                let parts = self.parts();\n                let mut items = SmallVec::with_capacity(parts.len());\n                for part in parts {\n                    let str_id = heap.allocate(HeapData::Str(Str::new(part.to_owned())))?;\n                    items.push(Value::Ref(str_id));\n                }\n                allocate_tuple(items, heap)?\n            }\n            _ => return Ok(None),\n        };\n        Ok(Some(v))\n    }\n}\n\nimpl PyTrait for Path {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Path\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        // Paths don't have a length in Python\n        None\n    }\n\n    fn py_eq(&self, other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Ok(self.path == other.path)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        // Paths are always truthy (even empty paths)\n        true\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        // Format like: PosixPath('/usr/bin')\n        write!(f, \"PosixPath('{}')\", self.path)\n    }\n\n    /// Handles attribute calls on Path objects, including both pure methods (no I/O)\n    /// and OS methods that require host system access.\n    ///\n    /// OS methods (exists, read_text, etc.) are detected via `OsFunction::try_from`\n    /// and returned as `CallResult::OsCall` for the VM to yield to the host.\n    /// Pure methods (is_absolute, joinpath, etc.) are handled directly.\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let heap = &mut *vm.heap;\n        let interns = vm.interns;\n        let Some(method) = attr.static_string() else {\n            args.drop_with_heap(heap);\n            return Err(ExcType::attribute_error(Type::Path, attr.as_str(interns)));\n        };\n\n        // Check if this is an OS method that requires host system access\n        if let Ok(os_fn) = OsFunction::try_from(method) {\n            // Package path as first argument for OS call (as Path, not string)\n            let path_arg = Value::Ref(heap.allocate(HeapData::Path(self.clone()))?);\n            let os_args = prepend_path_arg(path_arg, args);\n            return Ok(CallResult::OsCall(os_fn, os_args));\n        }\n\n        // Pure methods (no I/O)\n        let value = match method {\n            StaticStrings::IsAbsolute => {\n                args.check_zero_args(\"is_absolute\", heap)?;\n                Ok(Value::Bool(self.is_absolute()))\n            }\n            StaticStrings::Joinpath => {\n                let pos_args = args.into_pos_only(\"joinpath\", heap)?;\n                defer_drop!(pos_args, heap);\n                let path = fold_joinpath(self.clone(), pos_args.as_slice(), heap, interns)?;\n                Ok(Value::Ref(heap.allocate(HeapData::Path(path))?))\n            }\n            StaticStrings::WithName => {\n                let name_val = args.get_one_arg(\"with_name\", heap)?;\n                defer_drop!(name_val, heap);\n                let name = extract_path_string(name_val, heap, interns)?;\n                let result = self\n                    .with_name(name)\n                    .map_err(|e| crate::exception_private::SimpleException::new_msg(ExcType::ValueError, &e))?;\n                Ok(Value::Ref(heap.allocate(HeapData::Path(Self::new(result)))?))\n            }\n            StaticStrings::WithStem => {\n                let stem_val = args.get_one_arg(\"with_stem\", heap)?;\n                defer_drop!(stem_val, heap);\n                let stem = extract_path_string(stem_val, heap, interns)?;\n                let result = self\n                    .with_stem(stem)\n                    .map_err(|e| crate::exception_private::SimpleException::new_msg(ExcType::ValueError, &e))?;\n                Ok(Value::Ref(heap.allocate(HeapData::Path(Self::new(result)))?))\n            }\n            StaticStrings::WithSuffix => {\n                let suffix_val = args.get_one_arg(\"with_suffix\", heap)?;\n                defer_drop!(suffix_val, heap);\n                let suffix = extract_path_string(suffix_val, heap, interns)?;\n                let result = self\n                    .with_suffix(suffix)\n                    .map_err(|e| crate::exception_private::SimpleException::new_msg(ExcType::ValueError, &e))?;\n                Ok(Value::Ref(heap.allocate(HeapData::Path(Self::new(result)))?))\n            }\n            StaticStrings::AsPosix | StaticStrings::Fspath => {\n                args.check_zero_args(method.into(), heap)?;\n                // Both as_posix() and __fspath__() return the string representation\n                Ok(Value::Ref(\n                    heap.allocate(HeapData::Str(Str::new(self.as_posix().to_owned())))?,\n                ))\n            }\n            _ => {\n                args.drop_with_heap(heap);\n                return Err(ExcType::attribute_error(Type::Path, attr.as_str(interns)));\n            }\n        };\n        value.map(CallResult::Value)\n    }\n\n    fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<CallResult>> {\n        // Fast path: interned strings can be matched by ID without string comparison\n        if let Some(ss) = attr.static_string() {\n            if let Some(v) = self.getattr_by_static(ss, vm.heap)? {\n                return Ok(Some(CallResult::Value(v)));\n            }\n            return Err(ExcType::attribute_error(Type::Path, attr.as_str(vm.interns)));\n        }\n        // Slow path: heap-allocated strings need string comparison\n        let attr_str = attr.as_str(vm.interns);\n        let ss = match attr_str {\n            \"name\" => StaticStrings::Name,\n            \"parent\" => StaticStrings::Parent,\n            \"stem\" => StaticStrings::Stem,\n            \"suffix\" => StaticStrings::Suffix,\n            \"suffixes\" => StaticStrings::Suffixes,\n            \"parts\" => StaticStrings::Parts,\n            _ => return Err(ExcType::attribute_error(Type::Path, attr_str)),\n        };\n        let v = self\n            .getattr_by_static(ss, vm.heap)?\n            .expect(\"matched attribute must produce a value\");\n        Ok(Some(CallResult::Value(v)))\n    }\n}\n\nimpl HeapItem for Path {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.path.capacity()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // Path doesn't contain heap references, nothing to do\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/property.rs",
    "content": "//! Python property descriptor for computed attributes.\n//!\n//! Properties are descriptors whose value is computed when accessed.\n//! When a Property is retrieved via `py_getattr`, its getter is invoked\n//! rather than returning the Property itself.\n\nuse crate::{args::ArgValues, bytecode::CallResult, os::OsFunction};\n\n/// Property descriptor for computed attributes.\n///\n/// This mirrors Python's descriptor protocol for properties. When accessed,\n/// the property's getter is invoked to compute the value.\n///\n/// # Variants\n///\n/// Currently only supports OS properties. Future variants:\n/// - `Callable(FunctionId)` - user-defined getter functions (@property)\n/// - `External(StringId)` - external function getters\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) enum Property {\n    /// A property backed by an OS function (e.g., `os.environ`).\n    Os(OsFunction),\n}\n\nimpl Property {\n    /// Invokes the property getter, returning the appropriate `CallResult`.\n    ///\n    /// For OS properties, returns `CallResult::OsCall` to signal the VM\n    /// should yield to the host for the value.\n    pub fn get(self) -> CallResult {\n        match self {\n            Self::Os(os_fn) => CallResult::OsCall(os_fn, ArgValues::Empty),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/py_trait.rs",
    "content": "/// Trait for heap-allocated Python values that need common operations.\n///\n/// This trait abstracts over container types (List, Tuple, Str, Bytes) stored\n/// in the heap, providing a unified interface for operations like length,\n/// equality, reference counting support, and attribute dispatch.\n///\n/// The trait is designed to work with `enum_dispatch` for efficient virtual\n/// dispatch on `HeapData` without boxing overhead.\nuse std::borrow::Cow;\nuse std::{cmp::Ordering, fmt::Write};\n\nuse ahash::AHashSet;\n\nuse super::Type;\nuse crate::{\n    ResourceError,\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    exception_private::{ExcType, RunResult, SimpleException},\n    heap::{DropWithHeap, Heap, HeapId},\n    resource::ResourceTracker,\n    value::{EitherStr, Value},\n};\n\n/// Common operations for heap-allocated Python values.\n///\n/// Implementers should provide Python-compatible semantics for all operations.\n/// Most methods take a `&VM` or `&mut VM` reference to access the heap and interned\n/// strings for nested lookups in containers holding `Value::Ref` values.\n///\n/// This trait is used with `enum_dispatch` on `HeapData` to enable efficient\n/// virtual dispatch without boxing overhead.\n///\n/// Many methods are generic over `T: ResourceTracker` to work with any heap\n/// configuration. This allows the same trait to work with both unlimited and\n/// resource-limited execution contexts.\npub trait PyTrait {\n    /// Returns the Python type name for this value (e.g., \"list\", \"str\").\n    ///\n    /// Used for error messages and the `type()` builtin.\n    /// Takes heap reference for cases where nested Value lookups are needed.\n    fn py_type(&self, heap: &Heap<impl ResourceTracker>) -> Type;\n\n    /// Returns the number of elements in this container.\n    ///\n    /// For interns, returns the number of Unicode codepoints (characters), matching Python.\n    /// Returns `None` if the type doesn't support `len()`.\n    fn py_len(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize>;\n\n    /// Python equality comparison (`==`).\n    ///\n    /// For containers, this performs element-wise comparison using the heap\n    /// to resolve nested references. Takes `&mut VM` to allow lazy hash\n    /// computation for dict key lookups and access to interned string content.\n    ///\n    /// Recursion depth is tracked via `heap.incr_recursion_depth()`.\n    ///\n    /// Returns `Ok(true)` if equal, `Ok(false)` if not equal, or\n    /// `Err(ResourceError::Recursion)` if maximum depth is exceeded.\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError>;\n\n    /// Python comparison (`<`, `>`, etc.).\n    ///\n    /// For containers, this performs element-wise comparison using the heap\n    /// to resolve nested references. Takes `&mut VM` to allow lazy hash\n    /// computation for dict key lookups and access to interned string content.\n    ///\n    /// Recursion depth is tracked via `heap.incr_recursion_depth()`.\n    ///\n    /// Returns `Ok(Some(Ordering))` for comparable values, `Ok(None)` if not comparable,\n    /// or `Err(ResourceError::Recursion)` if maximum depth is exceeded.\n    fn py_cmp(\n        &self,\n        _other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Ordering>, ResourceError> {\n        Ok(None)\n    }\n\n    /// Returns the truthiness of the value following Python semantics.\n    ///\n    /// Container types should typically report `false` when empty.\n    fn py_bool(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        self.py_len(vm) != Some(0)\n    }\n\n    /// Writes the Python `repr()` string for this value to a formatter.\n    ///\n    /// This method enables cycle detection for self-referential structures by tracking\n    /// visited heap IDs. When a cycle is detected (ID already in `heap_ids`), implementations\n    /// should write an ellipsis (e.g., `[...]` for lists, `{...}` for dicts).\n    ///\n    /// Recursion depth is tracked via `heap.incr_recursion_depth_for_repr()`.\n    ///\n    /// # Arguments\n    /// * `f` - The formatter to write to\n    /// * `vm` - The VM for resolving value references and looking up interned strings\n    /// * `heap_ids` - Set of heap IDs currently being repr'd (for cycle detection)\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result;\n\n    /// Returns the Python `repr()` string for this value.\n    ///\n    /// Convenience wrapper around `py_repr_fmt` that returns an owned string.\n    fn py_repr(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Cow<'static, str> {\n        let mut s = String::new();\n        let mut heap_ids = AHashSet::new();\n        // Unwrap is safe: writing to String never fails\n        self.py_repr_fmt(&mut s, vm, &mut heap_ids).unwrap();\n        Cow::Owned(s)\n    }\n\n    /// Returns the Python `str()` string for this value.\n    ///\n    /// Recursion depth is tracked via the heap's recursion depth counter.\n    fn py_str(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Cow<'static, str> {\n        self.py_repr(vm)\n    }\n\n    /// Python addition (`__add__`).\n    ///\n    /// Returns `Ok(None)` if the operation is not supported for these types,\n    /// `Ok(Some(value))` on success, or `Err(ResourceError)` if allocation fails.\n    fn py_add(\n        &self,\n        _other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, ResourceError> {\n        Ok(None)\n    }\n\n    /// Python subtraction (`__sub__`).\n    ///\n    /// Returns `Ok(None)` if the operation is not supported for these types,\n    /// `Ok(Some(value))` on success, or `Err(ResourceError)` if allocation fails.\n    fn py_sub(\n        &self,\n        _other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, ResourceError> {\n        Ok(None)\n    }\n\n    /// Python modulus (`__mod__`).\n    ///\n    /// Returns `Ok(None)` if the operation is not supported for these types,\n    /// `Ok(Some(value))` on success, or `Err(RunError)` if an error occurs.\n    fn py_mod(&self, _other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        Ok(None)\n    }\n\n    /// Optimized helper for `(a % b) == c` comparisons.\n    fn py_mod_eq(&self, _other: &Self, _right_value: i64) -> Option<bool> {\n        None\n    }\n\n    /// Python in-place addition (`__iadd__`).\n    ///\n    /// # Returns\n    ///\n    /// Returns `Ok(true)` if the operation was successful, `Ok(false)` if not supported,\n    /// or `Err(ResourceError)` if allocation fails.\n    fn py_iadd(\n        &mut self,\n        _other: &Value,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n        _self_id: Option<HeapId>,\n    ) -> Result<bool, ResourceError> {\n        Ok(false)\n    }\n\n    /// Python multiplication (`__mul__`).\n    ///\n    /// Returns `Ok(None)` if the operation is not supported for these types.\n    /// For numeric types: Int * Int, Float * Float, Int * Float, etc.\n    /// For sequences: str * int, list * int for repetition.\n    fn py_mult(&self, _other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        Ok(None)\n    }\n\n    /// Python true division (`__truediv__`).\n    ///\n    /// Always returns float for numeric types. Returns `Ok(None)` if not supported.\n    /// Returns `Err(ZeroDivisionError)` for division by zero.\n    fn py_div(&self, _other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        Ok(None)\n    }\n\n    /// Python floor division (`__floordiv__`).\n    ///\n    /// Returns int for int//int, float for float operations.\n    /// Returns `Ok(None)` if not supported.\n    /// Returns `Err(ZeroDivisionError)` for division by zero.\n    fn py_floordiv(&self, _other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        Ok(None)\n    }\n\n    /// Python power (`__pow__`).\n    ///\n    /// Int ** positive_int returns int, int ** negative_int returns float.\n    /// Returns `Ok(None)` if not supported.\n    /// Returns `Err(ZeroDivisionError)` for 0 ** negative.\n    fn py_pow(&self, _other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        Ok(None)\n    }\n\n    /// Calls an attribute method on this value (e.g., `list.append()`), returning a\n    /// `CallResult` that may signal OS, external, or method calls.\n    ///\n    /// This method enables types to signal that they need operations the VM cannot perform\n    /// directly (OS operations, external function calls, dataclass method calls). The VM\n    /// converts the result to the appropriate `FrameExit` variant.\n    ///\n    /// Types that only support synchronous attribute calls should wrap their return value\n    /// with `CallResult::Value`. Types that need to perform OS/external operations,\n    /// intercept specific methods (e.g. `list.sort`), or detect method calls (e.g. dataclass\n    /// methods) should return the appropriate `CallResult` variant.\n    ///\n    /// # Arguments\n    /// * `self_id` - The heap ID of this value, needed by types that must reference themselves\n    ///   (e.g. dataclass method calls prepend `self` to args)\n    ///\n    /// # Returns\n    ///\n    /// - `Ok(CallResult::Value(v))` - Method completed synchronously with value `v`\n    /// - `Ok(CallResult::OsCall(func, args))` - Method needs OS operation; VM yields to host\n    /// - `Ok(CallResult::External(name, args))` - Method needs external function call\n    /// - `Ok(CallResult::MethodCall(attr, args))` - Dataclass method call; VM yields to host\n    /// - `Err(e)` - Method call failed with error\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        // `py_call_attr` takes ownership of the argument bundle. Implementations that\n        // do not recognize the attribute still need to release those values before\n        // reporting `AttributeError`, otherwise method calls on unsupported types leak\n        // references on the error path (caught by `ref-count-panic`).\n        args.drop_with_heap(vm);\n        Err(ExcType::attribute_error(self.py_type(vm.heap), attr.as_str(vm.interns)))\n    }\n\n    /// Python subscript get operation (`__getitem__`), e.g., `d[key]`.\n    ///\n    /// Returns the value associated with the key, or an error if the key doesn't exist\n    /// or the type doesn't support subscripting.\n    ///\n    /// Takes `&mut VM` for proper reference counting when cloning the returned value\n    /// and access to interned string content.\n    ///\n    /// Default implementation returns TypeError.\n    fn py_getitem(&self, _key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        Err(ExcType::type_error_not_sub(self.py_type(vm.heap)))\n    }\n\n    /// Python subscript set operation (`__setitem__`), e.g., `d[key] = value`.\n    ///\n    /// Sets the value associated with the key, or returns an error if the key is invalid\n    /// or the type doesn't support subscript assignment.\n    ///\n    /// Default implementation returns TypeError.\n    fn py_setitem(&mut self, key: Value, value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        key.drop_with_heap(vm.heap);\n        value.drop_with_heap(vm.heap);\n        Err(SimpleException::new_msg(\n            ExcType::TypeError,\n            format!(\"'{}' object does not support item assignment\", self.py_type(vm.heap)),\n        )\n        .into())\n    }\n\n    /// Python attribute get operation (`__getattr__`), e.g., `obj.attr`.\n    ///\n    /// Returns the value associated with the attribute (owned), or `Ok(None)` if the type\n    /// doesn't support attribute access at all. Types that support attributes should return\n    /// `Err(AttributeError)` when an attribute is not found, not `Ok(None)`.\n    ///\n    /// The returned `Value` is always owned:\n    /// - For stored values (Dataclass, Module, NamedTuple fields): clone with `clone_with_heap`\n    /// - For computed values (Exception.args, Slice.start, Path.name): return newly created value\n    ///\n    /// Takes `&mut VM` to allow:\n    /// - Cloning stored values with proper reference counting\n    /// - Allocating computed values that need heap storage\n    ///\n    /// Default implementation returns `Ok(None)`, indicating the type doesn't support\n    /// attribute access and a generic `AttributeError` should be raised by the caller.\n    fn py_getattr(\n        &self,\n        _attr: &EitherStr,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Option<CallResult>> {\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/range.rs",
    "content": "//! Python range type implementation.\n//!\n//! Provides a range object that supports iteration over a sequence of integers\n//! with configurable start, stop, and step values.\n\nuse std::fmt::Write;\n\nuse ahash::AHashSet;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapData, HeapId, HeapItem},\n    resource::{ResourceError, ResourceTracker},\n    types::{PyTrait, Type},\n    value::Value,\n};\n\n/// Python range object representing an immutable sequence of integers.\n///\n/// Supports three forms of construction:\n/// - `range(stop)` - integers from 0 to stop-1\n/// - `range(start, stop)` - integers from start to stop-1\n/// - `range(start, stop, step)` - integers from start, incrementing by step\n///\n/// The range is computed lazily during iteration, not stored as a list.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct Range {\n    /// The starting value (inclusive). Defaults to 0.\n    pub start: i64,\n    /// The ending value (exclusive).\n    pub stop: i64,\n    /// The step between values. Defaults to 1. Cannot be 0.\n    pub step: i64,\n}\n\nimpl Range {\n    /// Creates a new range with the given start, stop, and step.\n    ///\n    /// # Panics\n    /// Panics if step is 0. Use `new_checked` for fallible construction.\n    #[must_use]\n    fn new(start: i64, stop: i64, step: i64) -> Self {\n        debug_assert!(step != 0, \"range step cannot be 0\");\n        Self { start, stop, step }\n    }\n\n    /// Creates a range from just a stop value (start=0, step=1).\n    #[must_use]\n    fn from_stop(stop: i64) -> Self {\n        Self {\n            start: 0,\n            stop,\n            step: 1,\n        }\n    }\n\n    /// Creates a range from start and stop (step=1).\n    #[must_use]\n    fn from_start_stop(start: i64, stop: i64) -> Self {\n        Self { start, stop, step: 1 }\n    }\n\n    /// Returns the length of the range (number of elements it will yield).\n    #[must_use]\n    pub fn len(&self) -> usize {\n        if self.step > 0 {\n            if self.stop > self.start {\n                let len_i64 = (self.stop - self.start - 1) / self.step + 1;\n                usize::try_from(len_i64).expect(\"range length guaranteed non-negative\")\n            } else {\n                0\n            }\n        } else {\n            // step < 0\n            if self.start > self.stop {\n                let len_i64 = (self.start - self.stop - 1) / (-self.step) + 1;\n                usize::try_from(len_i64).expect(\"range length guaranteed non-negative\")\n            } else {\n                0\n            }\n        }\n    }\n\n    #[must_use]\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    /// Checks if an integer value is contained within this range (O(1)).\n    ///\n    /// A value is contained if it falls within the range bounds and is aligned\n    /// with the step (i.e., `(n - start) % step == 0`).\n    #[must_use]\n    pub fn contains(&self, n: i64) -> bool {\n        if self.step > 0 {\n            // Forward range: start <= n < stop\n            if n < self.start || n >= self.stop {\n                return false;\n            }\n        } else {\n            // Backward range: stop < n <= start\n            if n > self.start || n <= self.stop {\n                return false;\n            }\n        }\n        // Check if n is on the step grid\n        (n - self.start) % self.step == 0\n    }\n\n    /// Creates a range from the `range()` constructor call.\n    ///\n    /// Supports:\n    /// - `range(stop)` - range from 0 to stop\n    /// - `range(start, stop)` - range from start to stop\n    /// - `range(start, stop, step)` - range with custom step\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        let pos_args = args.into_pos_only(\"range\", heap)?;\n        defer_drop!(pos_args, heap);\n\n        let range = match pos_args.as_slice() {\n            [] => return Err(ExcType::type_error_at_least(\"range\", 1, 0)),\n            [first_arg] => {\n                let stop = first_arg.as_int(heap)?;\n                Self::from_stop(stop)\n            }\n            [first_arg, second_arg] => {\n                let start = first_arg.as_int(heap)?;\n                let stop = second_arg.as_int(heap)?;\n                Self::from_start_stop(start, stop)\n            }\n            [first_arg, second_arg, third_arg] => {\n                let start = first_arg.as_int(heap)?;\n                let stop = second_arg.as_int(heap)?;\n                let step = third_arg.as_int(heap)?;\n                if step == 0 {\n                    return Err(ExcType::value_error_range_step_zero());\n                }\n                Self::new(start, stop, step)\n            }\n            _ => return Err(ExcType::type_error_at_most(\"range\", 3, pos_args.len())),\n        };\n\n        Ok(Value::Ref(heap.allocate(HeapData::Range(range))?))\n    }\n\n    /// Handles slice-based indexing for ranges.\n    ///\n    /// Returns a new range object representing the sliced view.\n    /// The new range has computed start, stop, and step values.\n    fn getitem_slice(&self, slice: &crate::types::Slice, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let range_len = self.len();\n        let (start, stop, step) = slice\n            .indices(range_len)\n            .map_err(|()| ExcType::value_error_slice_step_zero())?;\n\n        // Calculate the new range parameters\n        // new_start = self.start + start * self.step\n        // new_step = self.step * slice_step\n        // new_stop needs to be computed based on the number of elements\n\n        let new_step = self.step.saturating_mul(step);\n        let start_i64 = i64::try_from(start).expect(\"start index fits in i64\");\n        let new_start = self.start.saturating_add(start_i64.saturating_mul(self.step));\n\n        // Calculate the number of elements in the sliced range\n        // try_from succeeds for non-negative step; step==0 rejected by slice.indices()\n        let num_elements = if let Ok(step_usize) = usize::try_from(step) {\n            // Forward iteration\n            if start >= stop {\n                0\n            } else {\n                ((stop - start - 1) / step_usize) + 1\n            }\n        } else {\n            // Backward iteration\n            let step_abs = usize::try_from(-step).expect(\"step is negative so -step is positive\");\n            if stop > range_len {\n                // stop sentinel means \"go to the beginning\"\n                (start / step_abs) + 1\n            } else if start <= stop {\n                0\n            } else {\n                ((start - stop - 1) / step_abs) + 1\n            }\n        };\n\n        // new_stop = new_start + num_elements * new_step\n        let num_elements_i64 = i64::try_from(num_elements).expect(\"num_elements fits in i64\");\n        let new_stop = new_start.saturating_add(num_elements_i64.saturating_mul(new_step));\n\n        let new_range = Self::new(new_start, new_stop, new_step);\n        Ok(Value::Ref(heap.allocate(HeapData::Range(new_range))?))\n    }\n}\n\nimpl Default for Range {\n    fn default() -> Self {\n        Self::from_stop(0)\n    }\n}\n\nimpl PyTrait for Range {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Range\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.len())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        // Check for slice first (Value::Ref pointing to HeapData::Slice)\n        if let Value::Ref(id) = key\n            && let HeapData::Slice(slice) = vm.heap.get(*id)\n        {\n            // Clone the slice to release the borrow on heap before calling getitem_slice\n            let slice = slice.clone();\n            return self.getitem_slice(&slice, vm.heap);\n        }\n\n        // Extract integer index, accepting Int, Bool (True=1, False=0), and LongInt\n        let index = key.as_index(vm.heap, Type::Range)?;\n\n        // Get range length for normalization\n        let len = i64::try_from(self.len()).expect(\"range length exceeds i64::MAX\");\n        let normalized = if index < 0 { index + len } else { index };\n\n        // Bounds check\n        if normalized < 0 || normalized >= len {\n            return Err(ExcType::range_index_error());\n        }\n\n        // Calculate: start + normalized * step\n        // Use checked arithmetic to avoid overflow in intermediate calculations\n        let offset = normalized\n            .checked_mul(self.step)\n            .and_then(|v| self.start.checked_add(v))\n            .expect(\"range element calculation overflowed\");\n        Ok(Value::Int(offset))\n    }\n\n    fn py_eq(&self, other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        // Compare ranges by their actual sequences, not parameters.\n        // Two ranges are equal if they produce the same elements.\n        let len1 = self.len();\n        let len2 = other.len();\n        if len1 != len2 {\n            return Ok(false);\n        }\n        // Same length - compare first element and step (if non-empty)\n        if len1 == 0 {\n            return Ok(true); // Both empty\n        }\n        Ok(self.start == other.start && self.step == other.step)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        if self.step == 1 {\n            write!(f, \"range({}, {})\", self.start, self.stop)\n        } else {\n            write!(f, \"range({}, {}, {})\", self.start, self.stop, self.step)\n        }\n    }\n}\n\nimpl HeapItem for Range {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // Range doesn't contain heap references, nothing to do\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/re_match.rs",
    "content": "//! Regex match result type for the `re` module.\n//!\n//! `ReMatch` represents the result of a successful regex match operation.\n//! It stores the matched text, capture groups, and their positions, providing\n//! Python-compatible access via `.group()`, `.groups()`, `.start()`, `.end()`,\n//! and `.span()` methods.\n//!\n//! All data is stored as owned values (no heap references), so reference counting\n//! is trivial — `py_dec_ref_ids` is a no-op.\n\nuse std::{cmp::Ordering, fmt::Write};\n\nuse ahash::AHashSet;\nuse smallvec::smallvec;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapData, HeapId, HeapItem},\n    intern::{Interns, StaticStrings},\n    resource::{ResourceError, ResourceTracker},\n    types::{Dict, PyTrait, Str, Type, allocate_tuple, str::string_repr_fmt},\n    value::{EitherStr, Value},\n};\n\n/// A regex match result, storing captured groups and positions.\n///\n/// Created by `re.match()`, `re.search()`, `re.fullmatch()`, and their\n/// `Pattern` method equivalents. Stores all data as owned values (no heap\n/// references), which simplifies reference counting — `py_dec_ref_ids` is\n/// a no-op.\n///\n/// The `.re` attribute (reference back to the pattern) is intentionally omitted\n/// to avoid circular references between Match and Pattern objects.\n///\n/// # Position semantics\n///\n/// Positions are returned as Unicode character offsets (not byte offsets) to\n/// match CPython's behavior. The conversion from byte offsets (used internally\n/// by the Rust `regex` crate) happens at construction time in `from_captures`.\n///\n/// # Group Indexing\n///\n/// Group 0 is the full match, groups 1..N are capture groups.\n/// Both integer and named group access are supported — named groups are looked\n/// up via the `named_groups` mapping.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) struct ReMatch {\n    /// The full matched text (equivalent to `group(0)`).\n    full_match: String,\n    /// Start character position of the full match in the input string.\n    start: usize,\n    /// End character position of the full match in the input string.\n    end: usize,\n    /// Captured group strings (index 0 = group 1). `None` for unmatched optional groups.\n    groups: Vec<Option<String>>,\n    /// Span positions per captured group (index 0 = group 1). `None` for unmatched optional groups.\n    group_spans: Vec<Option<(usize, usize)>>,\n    /// Named groups: maps group name → 1-based group index.\n    named_groups: Vec<(String, usize)>,\n    /// Owned copy of the input string (returned by `.string` attribute).\n    input_string: String,\n    /// The original pattern string (used in repr output).\n    pattern_string: String,\n}\n\nimpl ReMatch {\n    /// Creates a `ReMatch` from a `fancy_regex::Captures` result.\n    ///\n    /// Converts byte offsets from the regex crate into character offsets to match\n    /// CPython's behavior. The full match (group 0) is always present when captures\n    /// are successful.\n    ///\n    /// # Arguments\n    /// * `caps` - The successful capture result from the regex engine\n    /// * `input` - The full input string that was searched\n    /// * `pattern` - The original pattern string (for repr)\n    /// * `regex` - The compiled regex, used to extract named group mappings\n    pub fn from_captures(\n        caps: &fancy_regex::Captures<'_>,\n        input: &str,\n        pattern: &str,\n        regex: &fancy_regex::Regex,\n    ) -> Self {\n        let full = caps.get(0).expect(\"group 0 always exists on a successful match\");\n        let full_match = full.as_str().to_owned();\n        let start = byte_to_char_offset(input, full.start());\n        let end = byte_to_char_offset(input, full.end());\n\n        let group_count = caps.len().saturating_sub(1);\n        let mut groups = Vec::with_capacity(group_count);\n        let mut group_spans = Vec::with_capacity(group_count);\n\n        for cap in caps.iter().skip(1) {\n            if let Some(m) = cap {\n                groups.push(Some(m.as_str().to_owned()));\n                group_spans.push(Some((\n                    byte_to_char_offset(input, m.start()),\n                    byte_to_char_offset(input, m.end()),\n                )));\n            } else {\n                groups.push(None);\n                group_spans.push(None);\n            }\n        }\n\n        // Extract named group name→index mappings from the regex\n        let mut named_groups = Vec::new();\n        for (idx, name) in regex.capture_names().enumerate() {\n            if let Some(name) = name {\n                named_groups.push((name.to_owned(), idx));\n            }\n        }\n\n        Self {\n            full_match,\n            start,\n            end,\n            groups,\n            group_spans,\n            named_groups,\n            input_string: input.to_owned(),\n            pattern_string: pattern.to_owned(),\n        }\n    }\n\n    /// Returns the match for a given group number.\n    ///\n    /// Group 0 is the full match, groups 1..N are capture groups.\n    /// Returns `Value::None` for unmatched optional groups.\n    /// Raises `IndexError` for invalid group numbers.\n    fn get_group(&self, n: i64, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        match n.cmp(&0) {\n            Ordering::Equal => {\n                let s = Str::new(self.full_match.clone());\n                Ok(Value::Ref(heap.allocate(HeapData::Str(s))?))\n            }\n            Ordering::Less => Err(ExcType::re_match_group_index_error()),\n            Ordering::Greater => {\n                let idx = group_index(n);\n                if idx >= self.groups.len() {\n                    return Err(ExcType::re_match_group_index_error());\n                }\n                match &self.groups[idx] {\n                    Some(s) => {\n                        let s = Str::new(s.clone());\n                        Ok(Value::Ref(heap.allocate(HeapData::Str(s))?))\n                    }\n                    None => Ok(Value::None),\n                }\n            }\n        }\n    }\n\n    /// Returns the match for a named group.\n    ///\n    /// Looks up the group name in `named_groups` and delegates to `get_group`.\n    /// Raises `IndexError` if the name is not found.\n    fn get_group_by_name(&self, name: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        for (group_name, idx) in &self.named_groups {\n            if group_name == name {\n                #[expect(clippy::cast_possible_wrap, reason = \"group indices are always small\")]\n                return self.get_group(*idx as i64, heap);\n            }\n        }\n        Err(ExcType::re_match_group_index_error())\n    }\n\n    /// Implements `m[key]` subscript access on match objects.\n    ///\n    /// Supports integer indexing (like `m[0]`, `m[1]`), bool indexing,\n    /// and string indexing for named groups (like `m['name']`).\n    pub fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        match key {\n            Value::Int(n) => self.get_group(*n, vm.heap),\n            Value::Bool(b) => self.get_group(i64::from(*b), vm.heap),\n            Value::InternString(id) => {\n                let name = vm.interns.get_str(*id);\n                self.get_group_by_name(name, vm.heap)\n            }\n            Value::Ref(heap_id) => match vm.heap.get(*heap_id) {\n                HeapData::Str(s) => {\n                    let name = s.as_str().to_owned();\n                    self.get_group_by_name(&name, vm.heap)\n                }\n                _ => Err(ExcType::re_match_group_index_error()),\n            },\n            _ => Err(ExcType::re_match_group_index_error()),\n        }\n    }\n\n    /// Returns a dict mapping named group names to their matched strings.\n    ///\n    /// Groups that didn't participate in the match have the `default` value\n    /// (typically `None`).\n    fn get_groupdict(&self, default: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        let mut pairs = Vec::with_capacity(self.named_groups.len());\n        for (name, idx) in &self.named_groups {\n            let key_str = Str::new(name.clone());\n            let key = Value::Ref(vm.heap.allocate(HeapData::Str(key_str))?);\n            // idx is 1-based, groups vec is 0-based (index 0 = group 1)\n            let value = if *idx > 0 && (*idx - 1) < self.groups.len() {\n                match &self.groups[*idx - 1] {\n                    Some(s) => {\n                        let s = Str::new(s.clone());\n                        Value::Ref(vm.heap.allocate(HeapData::Str(s))?)\n                    }\n                    None => default.clone_with_heap(vm),\n                }\n            } else {\n                default.clone_with_heap(vm)\n            };\n            pairs.push((key, value));\n        }\n        let dict = Dict::from_pairs(pairs, vm)?;\n        Ok(Value::Ref(vm.heap.allocate(HeapData::Dict(dict))?))\n    }\n\n    /// Returns a tuple of all capture group strings.\n    ///\n    /// Unmatched optional groups appear as `None`.\n    fn get_groups(&self, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let mut elements = smallvec![];\n        for group in &self.groups {\n            match group {\n                Some(s) => {\n                    let s = Str::new(s.clone());\n                    elements.push(Value::Ref(heap.allocate(HeapData::Str(s))?));\n                }\n                None => elements.push(Value::None),\n            }\n        }\n        Ok(allocate_tuple(elements, heap)?)\n    }\n\n    /// Returns the start character position for a given group.\n    ///\n    /// Group 0 is the full match. Returns -1 for unmatched optional groups\n    #[expect(clippy::cast_possible_wrap, reason = \"positions are always small enough for i64\")]\n    fn get_start(&self, n: i64) -> RunResult<Value> {\n        match n.cmp(&0) {\n            Ordering::Equal => Ok(Value::Int(self.start as i64)),\n            Ordering::Less => Err(ExcType::re_match_group_index_error()),\n            Ordering::Greater => {\n                let idx = group_index(n);\n                if idx >= self.group_spans.len() {\n                    return Err(ExcType::re_match_group_index_error());\n                }\n                match &self.group_spans[idx] {\n                    Some((s, _)) => Ok(Value::Int(*s as i64)),\n                    None => Ok(Value::Int(-1)),\n                }\n            }\n        }\n    }\n\n    /// Returns the end character position for a given group.\n    ///\n    /// Group 0 is the full match. Returns -1 for unmatched optional groups\n    #[expect(clippy::cast_possible_wrap, reason = \"positions are always small enough for i64\")]\n    fn get_end(&self, n: i64) -> RunResult<Value> {\n        match n.cmp(&0) {\n            Ordering::Equal => Ok(Value::Int(self.end as i64)),\n            Ordering::Less => Err(ExcType::re_match_group_index_error()),\n            Ordering::Greater => {\n                let idx = group_index(n);\n                if idx >= self.group_spans.len() {\n                    return Err(ExcType::re_match_group_index_error());\n                }\n                match &self.group_spans[idx] {\n                    Some((_, e)) => Ok(Value::Int(*e as i64)),\n                    None => Ok(Value::Int(-1)),\n                }\n            }\n        }\n    }\n\n    /// Returns a `(start, end)` tuple for a given group.\n    ///\n    /// Group 0 is the full match. Returns `(-1, -1)` for unmatched optional groups\n    #[expect(clippy::cast_possible_wrap, reason = \"positions are always small enough for i64\")]\n    fn get_span(&self, n: i64, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        match n.cmp(&0) {\n            Ordering::Equal => Ok(allocate_tuple(\n                smallvec![Value::Int(self.start as i64), Value::Int(self.end as i64)],\n                heap,\n            )?),\n            Ordering::Less => Err(ExcType::re_match_group_index_error()),\n            Ordering::Greater => {\n                let idx = group_index(n);\n                if idx >= self.group_spans.len() {\n                    return Err(ExcType::re_match_group_index_error());\n                }\n                let (s, e) = match &self.group_spans[idx] {\n                    Some((s, e)) => (*s as i64, *e as i64),\n                    None => (-1, -1),\n                };\n                Ok(allocate_tuple(smallvec![Value::Int(s), Value::Int(e)], heap)?)\n            }\n        }\n    }\n}\n\nimpl PyTrait for ReMatch {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::ReMatch\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        None\n    }\n\n    fn py_eq(&self, _other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        // Match objects are not comparable\n        Ok(false)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        // Match objects are always truthy\n        true\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        write!(f, \"<re.Match object; span=({}, {}), match=\", self.start, self.end)?;\n        string_repr_fmt(&self.full_match, f)?;\n        f.write_char('>')\n    }\n\n    fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<CallResult>> {\n        match attr.static_string() {\n            Some(StaticStrings::StringAttr) => {\n                let s = Str::new(self.input_string.clone());\n                let v = Value::Ref(vm.heap.allocate(HeapData::Str(s))?);\n                Ok(Some(CallResult::Value(v)))\n            }\n            _ => Err(ExcType::attribute_error(Type::ReMatch, attr.as_str(vm.interns))),\n        }\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let result = match attr.static_string() {\n            Some(StaticStrings::Group) => call_group(self, args, vm.heap, vm.interns)?,\n            Some(StaticStrings::Groups) => {\n                args.check_zero_args(\"re.Match.groups\", vm.heap)?;\n                self.get_groups(vm.heap)?\n            }\n            Some(StaticStrings::Groupdict) => {\n                let default = args.get_zero_one_arg(\"re.Match.groupdict\", vm.heap)?;\n                let default = default.unwrap_or(Value::None);\n                let result = self.get_groupdict(&default, vm)?;\n                default.drop_with_heap(vm.heap);\n                result\n            }\n            Some(StaticStrings::Start) => {\n                let n = extract_optional_group_arg(args, \"re.Match.start\", 0, vm.heap)?;\n                self.get_start(n)?\n            }\n            Some(StaticStrings::End) => {\n                let n = extract_optional_group_arg(args, \"re.Match.end\", 0, vm.heap)?;\n                self.get_end(n)?\n            }\n            Some(StaticStrings::Span) => {\n                let n = extract_optional_group_arg(args, \"re.Match.span\", 0, vm.heap)?;\n                self.get_span(n, vm.heap)?\n            }\n            _ => return Err(ExcType::attribute_error(Type::ReMatch, attr.as_str(vm.interns))),\n        };\n        Ok(CallResult::Value(result))\n    }\n}\n\nimpl HeapItem for ReMatch {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n            + self.full_match.len()\n            + self.input_string.len()\n            + self.pattern_string.len()\n            + self\n                .groups\n                .iter()\n                .map(|g| g.as_ref().map_or(0, String::len))\n                .sum::<usize>()\n            + self\n                .named_groups\n                .iter()\n                .map(|(name, _)| name.len() + std::mem::size_of::<usize>())\n                .sum::<usize>()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // No heap references — all data is owned strings and integers.\n    }\n}\n\n/// Handles `m.group(...)` calls, supporting zero, one, or multiple arguments.\n///\n/// - `m.group()` → equivalent to `m.group(0)`, returns full match string\n/// - `m.group(n)` → returns the nth group (integer or named string)\n/// - `m.group(n1, n2, ...)` → returns a tuple of groups\nfn call_group(\n    m: &ReMatch,\n    args: ArgValues,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Value> {\n    match args {\n        ArgValues::Empty => m.get_group(0, heap),\n        ArgValues::One(v) => {\n            let result = resolve_group_arg(m, &v, heap, interns);\n            v.drop_with_heap(heap);\n            result\n        }\n        other => {\n            let pos = other.into_pos_only(\"re.Match.group\", heap)?;\n            let mut pos_guard = smallvec::SmallVec::<[Value; 4]>::new();\n            for val in pos {\n                pos_guard.push(val);\n            }\n            let mut elements = smallvec::smallvec![];\n            for val in &pos_guard {\n                let result = resolve_group_arg(m, val, heap, interns);\n                if result.is_err() {\n                    // Drop already-allocated elements\n                    for elem in elements {\n                        Value::drop_with_heap(elem, heap);\n                    }\n                    for val in pos_guard {\n                        val.drop_with_heap(heap);\n                    }\n                    return result;\n                }\n                elements.push(result?);\n            }\n            for val in pos_guard {\n                val.drop_with_heap(heap);\n            }\n            Ok(allocate_tuple(elements, heap)?)\n        }\n    }\n}\n\n/// Resolves a single group argument — integer, bool, or string (named group).\nfn resolve_group_arg(\n    m: &ReMatch,\n    val: &Value,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Value> {\n    match val {\n        Value::Int(n) => m.get_group(*n, heap),\n        Value::Bool(b) => m.get_group(i64::from(*b), heap),\n        Value::InternString(id) => {\n            let name = interns.get_str(*id);\n            m.get_group_by_name(name, heap)\n        }\n        Value::Ref(heap_id) => match heap.get(*heap_id) {\n            HeapData::Str(s) => {\n                let name = s.as_str().to_owned();\n                m.get_group_by_name(&name, heap)\n            }\n            _ => Err(ExcType::re_match_group_index_error()),\n        },\n        _ => Err(ExcType::re_match_group_index_error()),\n    }\n}\n\n/// Extracts an optional integer argument for group-related methods.\n///\n/// Many `re.Match` methods accept an optional group number that defaults to 0.\n/// This helper extracts the argument, validates it is an integer (or string for\n/// named groups), and returns the group number.\nfn extract_optional_group_arg(\n    args: ArgValues,\n    name: &str,\n    default: i64,\n    heap: &mut Heap<impl ResourceTracker>,\n) -> RunResult<i64> {\n    let opt = args.get_zero_one_arg(name, heap)?;\n    match opt {\n        None => Ok(default),\n        Some(Value::Int(n)) => Ok(n),\n        // CPython treats bool as int subclass: True=1, False=0.\n        Some(Value::Bool(b)) => Ok(i64::from(b)),\n        // String group names are not valid for start/end/span — they take integers only\n        Some(other) => {\n            other.drop_with_heap(heap);\n            Err(ExcType::re_match_group_index_error())\n        }\n    }\n}\n\n/// Converts a byte offset in a UTF-8 string to a character (code point) offset.\n///\n/// The Rust `regex` crate operates on byte offsets, but Python's `re` module\n/// returns character positions. For ASCII-only strings, these are identical.\n/// For multi-byte UTF-8 characters, this counts actual code points up to the\n/// byte position.\nfn byte_to_char_offset(s: &str, byte_offset: usize) -> usize {\n    s[..byte_offset].chars().count()\n}\n\n/// Converts a positive group number (1-based) to a 0-based index.\n///\n/// The caller must ensure `n > 0`.\n#[expect(\n    clippy::cast_sign_loss,\n    clippy::cast_possible_truncation,\n    reason = \"n is always positive (checked by caller via match on Ordering::Greater)\"\n)]\nfn group_index(n: i64) -> usize {\n    (n - 1) as usize\n}\n"
  },
  {
    "path": "crates/monty/src/types/re_pattern.rs",
    "content": "//! Compiled regex pattern type for the `re` module.\n//!\n//! `RePattern` wraps a compiled `fancy_regex::Regex` with the original Python pattern\n//! string and flags. The `fancy_regex` crate supports backreferences, lookahead/lookbehind,\n//! and other advanced features, but uses backtracking which means patterns are susceptible\n//! to ReDoS. Monty's resource limits (time and allocation budgets) are the primary defense\n//! against catastrophic backtracking in untrusted patterns.\n//!\n//! Custom serde serializes only the pattern string and flags, recompiling the regex\n//! on deserialization. This supports Monty's snapshot/restore feature.\n\nuse std::{borrow::Cow, fmt::Write};\n\nuse ahash::AHashSet;\nuse fancy_regex::Regex;\nuse serde::{Deserialize, Deserializer, Serialize, Serializer};\nuse smallvec::SmallVec;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult},\n    heap::{DropWithHeap, Heap, HeapData, HeapId, HeapItem},\n    intern::{Interns, StaticStrings},\n    modules::re::{ASCII, DOTALL, IGNORECASE, MULTILINE},\n    resource::{ResourceError, ResourceTracker, check_estimated_size},\n    types::{List, PyTrait, ReMatch, Str, Type, allocate_tuple, str::string_repr_fmt},\n    value::{EitherStr, Value},\n};\n\n/// A compiled regular expression pattern.\n///\n/// Wraps a `fancy_regex::Regex` with the original Python pattern string and flags.\n/// The `fancy_regex` crate supports backtracking features like backreferences and\n/// lookaround, but this means patterns are susceptible to ReDoS — Monty's resource\n/// limits are the defense against catastrophic backtracking.\n///\n/// Custom serde serializes only the pattern string and flags, recompiling the\n/// regex on deserialization. This supports Monty's snapshot/restore feature.\n#[derive(Debug)]\npub(crate) struct RePattern {\n    /// The original Python regex pattern string.\n    pattern: String,\n    /// Python regex flags bitmask (IGNORECASE=2, MULTILINE=8, DOTALL=16, ASCII=256).\n    flags: u16,\n    /// The compiled Rust regex, unanchored.\n    compiled: Regex,\n    /// The compiled regex anchored with `\\A(?:...)` for `match()`.\n    ///\n    /// Uses `\\A` (absolute start anchor) instead of `^` so the MULTILINE flag\n    /// doesn't cause it to match at line boundaries. This correctly handles\n    /// alternations — e.g. `match('b|ab', 'ab')` must match `ab`, not fail\n    /// because the engine found only `b` starting at position 1.\n    compiled_match: Regex,\n    /// The compiled regex anchored with `\\A(?:...)\\z` for `fullmatch()`.\n    ///\n    /// Uses `\\A`/`\\z` (absolute anchors) instead of `^`/`$` so the MULTILINE flag\n    /// doesn't cause them to match at line boundaries. This correctly handles\n    /// alternations — e.g. `fullmatch('a|ab', 'ab')` must match `ab`, not fail\n    /// because the engine found `a` first.\n    compiled_fullmatch: Regex,\n}\n\nimpl RePattern {\n    /// Creates a compiled pattern from a Python regex string and flags.\n    ///\n    /// Translates Python flag constants into inline regex flag prefixes and compiles\n    /// the pattern. Also pre-compiles anchored variants for `match` (`\\A(?:pattern)`)\n    /// and `fullmatch` (`\\A(?:pattern)\\z`) to correctly handle alternations.\n    ///\n    /// # Errors\n    ///\n    /// Returns `re.PatternError` if the pattern is invalid.\n    pub fn compile(pattern: String, flags: u16) -> RunResult<Self> {\n        let compiled = compile_regex(&pattern, flags)?;\n        let compiled_match = compile_regex(&format!(\"\\\\A(?:{pattern})\"), flags)?;\n        let compiled_fullmatch = compile_regex(&format!(\"\\\\A(?:{pattern})\\\\z\"), flags)?;\n        Ok(Self {\n            pattern,\n            flags,\n            compiled,\n            compiled_match,\n            compiled_fullmatch,\n        })\n    }\n\n    /// `pattern.search(string)` — find first match anywhere in the string.\n    ///\n    /// Returns a `ReMatch` heap object on success, or `Value::None` if no match.\n    pub fn search(&self, text: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        match self.compiled.captures(text) {\n            Ok(Some(caps)) => {\n                let m = ReMatch::from_captures(&caps, text, &self.pattern, &self.compiled);\n                Ok(Value::Ref(heap.allocate(HeapData::ReMatch(m))?))\n            }\n            Ok(None) => Ok(Value::None),\n            Err(err) => Err(ExcType::re_pattern_error(err)),\n        }\n    }\n\n    /// `pattern.match(string)` — match anchored at the start of the string.\n    ///\n    /// Uses a pre-compiled `\\A(?:pattern)` regex to correctly handle alternations.\n    /// For example, `match('b|ab', 'ab')` correctly matches `ab` because the\n    /// anchor forces the engine to try all alternatives at position 0.\n    ///\n    /// Returns a `ReMatch` heap object on success, or `Value::None` if no match.\n    pub fn match_start(&self, text: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        match self.compiled_match.captures(text) {\n            Ok(Some(caps)) => {\n                let match_obj = ReMatch::from_captures(&caps, text, &self.pattern, &self.compiled);\n                Ok(Value::Ref(heap.allocate(HeapData::ReMatch(match_obj))?))\n            }\n            Ok(None) => Ok(Value::None),\n            Err(err) => Err(ExcType::re_pattern_error(err)),\n        }\n    }\n\n    /// `pattern.fullmatch(string)` — match the entire string.\n    ///\n    /// Uses a pre-compiled `\\A(?:pattern)\\z` regex to correctly handle alternations.\n    /// For example, `fullmatch('a|ab', 'ab')` correctly matches `ab` because the\n    /// anchors force the engine to try all alternatives for a full-string match.\n    ///\n    /// Returns a `ReMatch` heap object on success, or `Value::None` if no match.\n    pub fn fullmatch(&self, text: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        match self.compiled_fullmatch.captures(text) {\n            Ok(Some(caps)) => {\n                let match_obj = ReMatch::from_captures(&caps, text, &self.pattern, &self.compiled);\n                Ok(Value::Ref(heap.allocate(HeapData::ReMatch(match_obj))?))\n            }\n            Ok(None) => Ok(Value::None),\n            Err(err) => Err(ExcType::re_pattern_error(err)),\n        }\n    }\n\n    /// `pattern.findall(string)` — return all non-overlapping matches.\n    ///\n    /// Follows CPython's semantics:\n    /// - No capture groups: returns a list of matched strings\n    /// - One capture group: returns a list of the group's matched strings\n    /// - Multiple capture groups: returns a list of tuples of matched strings\n    pub fn findall(&self, text: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let cap_count = self.compiled.captures_len();\n        let mut results = Vec::new();\n\n        match cap_count {\n            // No capture groups — return list of full match strings\n            0 | 1 => {\n                for m in self.compiled.find_iter(text) {\n                    let s = Str::new(m.map_err(ExcType::re_pattern_error)?.as_str().to_owned());\n                    results.push(Value::Ref(heap.allocate(HeapData::Str(s))?));\n                }\n            }\n            // One capture group — return list of the group's strings\n            2 => {\n                for caps in self.compiled.captures_iter(text) {\n                    let caps = caps.map_err(ExcType::re_pattern_error)?;\n                    let val = caps.get(1).map(|m| m.as_str().to_owned()).unwrap_or_default();\n                    let s = Str::new(val);\n                    results.push(Value::Ref(heap.allocate(HeapData::Str(s))?));\n                }\n            }\n            // Multiple capture groups — return list of tuples\n            _ => {\n                for caps in self.compiled.captures_iter(text) {\n                    let caps = caps.map_err(ExcType::re_pattern_error)?;\n                    let mut elements: SmallVec<[Value; 3]> = SmallVec::with_capacity(cap_count - 1);\n                    for cap in caps.iter().skip(1) {\n                        let val = cap.map(|m| m.as_str().to_owned()).unwrap_or_default();\n                        let s = Str::new(val);\n                        elements.push(Value::Ref(heap.allocate(HeapData::Str(s))?));\n                    }\n                    results.push(allocate_tuple(elements, heap)?);\n                }\n            }\n        }\n\n        let list = List::new(results);\n        Ok(Value::Ref(heap.allocate(HeapData::List(list))?))\n    }\n\n    /// `pattern.sub(repl, string, count=0)` — substitute matches with a replacement.\n    ///\n    /// When `count` is 0, all matches are replaced. Otherwise, at most `count`\n    /// replacements are made. The replacement string supports `$1`, `$2`, etc.\n    /// for backreferences to captured groups.\n    ///\n    /// Builds the result string in a single pass by iterating matches and appending\n    /// replacements directly. Checks the running output size against resource limits\n    /// after each match, bailing out immediately if the budget is exceeded. This\n    /// avoids both false rejections from conservative pre-estimates and untracked\n    /// Rust heap allocations from delegating to `fancy_regex::replace_all()`.\n    pub fn sub(&self, repl: &str, text: &str, count: usize, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        // Translate Python-style backreferences (\\1, \\2) to regex crate style ($1, $2)\n        let rust_repl = translate_replacement(repl);\n        let effective_count = if count == 0 { usize::MAX } else { count };\n\n        let mut result = String::new();\n        let mut last_end = 0;\n\n        for caps in self.compiled.captures_iter(text).take(effective_count) {\n            let caps = caps.map_err(ExcType::re_pattern_error)?;\n            let m = caps.get(0).expect(\"capture group 0 always exists\");\n            result.push_str(&text[last_end..m.start()]);\n            caps.expand(rust_repl.as_ref(), &mut result);\n            last_end = m.end();\n            // Check running size: current result + remaining unprocessed text.\n            check_estimated_size(result.len() + (text.len() - last_end), heap.tracker())?;\n        }\n\n        result.push_str(&text[last_end..]);\n        let s = Str::new(result);\n        Ok(Value::Ref(heap.allocate(HeapData::Str(s))?))\n    }\n\n    /// `pattern.split(string, maxsplit=0)` — split string by pattern occurrences.\n    ///\n    /// Returns a list of strings. If `maxsplit` is non-zero, at most `maxsplit`\n    /// splits occur and the remainder of the string is returned as the final element.\n    pub fn split(&self, text: &str, maxsplit: usize, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let pieces: Vec<&str> = if maxsplit == 0 {\n            self.compiled\n                .split(text)\n                .collect::<Result<Vec<_>, _>>()\n                .map_err(ExcType::re_pattern_error)?\n        } else {\n            self.compiled\n                .splitn(text, maxsplit + 1)\n                .collect::<Result<Vec<_>, _>>()\n                .map_err(ExcType::re_pattern_error)?\n        };\n\n        let mut results = Vec::with_capacity(pieces.len());\n        for piece in pieces {\n            let s = Str::new(piece.to_owned());\n            results.push(Value::Ref(heap.allocate(HeapData::Str(s))?));\n        }\n\n        let list = List::new(results);\n        Ok(Value::Ref(heap.allocate(HeapData::List(list))?))\n    }\n\n    /// `pattern.finditer(string)` — return all matches as a list.\n    ///\n    /// Eagerly collects all match objects into a list. This differs from CPython's\n    /// lazy iterator but produces the same results when iterated. The VM's `GetIter`\n    /// opcode handles iteration over the returned list.\n    pub fn finditer(&self, text: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let mut results = Vec::new();\n        for caps in self.compiled.captures_iter(text) {\n            let caps = caps.map_err(ExcType::re_pattern_error)?;\n            let m = ReMatch::from_captures(&caps, text, &self.pattern, &self.compiled);\n            results.push(Value::Ref(heap.allocate(HeapData::ReMatch(m))?));\n        }\n\n        let list = List::new(results);\n        Ok(Value::Ref(heap.allocate(HeapData::List(list))?))\n    }\n}\n\nimpl PyTrait for RePattern {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::RePattern\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        None\n    }\n\n    fn py_eq(&self, other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Ok(self.pattern == other.pattern && self.flags == other.flags)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        // Pattern objects are always truthy (matching CPython).\n        true\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        write!(f, \"re.compile(\")?;\n        string_repr_fmt(&self.pattern, f)?;\n        if self.flags != 0 {\n            let mut flag_parts = smallvec::SmallVec::<[&'static str; 4]>::new();\n            if self.flags & IGNORECASE != 0 {\n                flag_parts.push(\"re.IGNORECASE\");\n            }\n            if self.flags & MULTILINE != 0 {\n                flag_parts.push(\"re.MULTILINE\");\n            }\n            if self.flags & DOTALL != 0 {\n                flag_parts.push(\"re.DOTALL\");\n            }\n            if self.flags & ASCII != 0 {\n                flag_parts.push(\"re.ASCII\");\n            }\n            write!(f, \", {}\", flag_parts.join(\"|\"))?;\n        }\n        write!(f, \")\")\n    }\n\n    fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<CallResult>> {\n        match attr.static_string() {\n            Some(StaticStrings::PatternAttr) => {\n                let s = Str::new(self.pattern.clone());\n                let v = Value::Ref(vm.heap.allocate(HeapData::Str(s))?);\n                Ok(Some(CallResult::Value(v)))\n            }\n            Some(StaticStrings::Flags) => Ok(Some(CallResult::Value(Value::Int(i64::from(self.flags))))),\n            _ => Err(ExcType::attribute_error(Type::RePattern, attr.as_str(vm.interns))),\n        }\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let result = match attr.static_string() {\n            Some(StaticStrings::Search) => {\n                let arg = args.get_one_arg(\"Pattern.search\", vm.heap)?;\n                defer_drop!(arg, vm);\n                let text = value_to_str(arg, vm.heap, vm.interns)?.into_owned();\n                self.search(&text, vm.heap)\n            }\n            Some(StaticStrings::Match) => {\n                let arg = args.get_one_arg(\"Pattern.match\", vm.heap)?;\n                defer_drop!(arg, vm);\n                let text = value_to_str(arg, vm.heap, vm.interns)?.into_owned();\n                self.match_start(&text, vm.heap)\n            }\n            Some(StaticStrings::Fullmatch) => {\n                let arg = args.get_one_arg(\"Pattern.fullmatch\", vm.heap)?;\n                defer_drop!(arg, vm);\n                let text = value_to_str(arg, vm.heap, vm.interns)?.into_owned();\n                self.fullmatch(&text, vm.heap)\n            }\n            Some(StaticStrings::Findall) => {\n                let arg = args.get_one_arg(\"Pattern.findall\", vm.heap)?;\n                defer_drop!(arg, vm);\n                let text = value_to_str(arg, vm.heap, vm.interns)?.into_owned();\n                self.findall(&text, vm.heap)\n            }\n            Some(StaticStrings::Sub) => call_pattern_sub(self, args, vm.heap, vm.interns),\n            Some(StaticStrings::Split) => call_pattern_split(self, args, vm.heap, vm.interns),\n            Some(StaticStrings::Finditer) => {\n                let arg = args.get_one_arg(\"Pattern.finditer\", vm.heap)?;\n                defer_drop!(arg, vm);\n                let text = value_to_str(arg, vm.heap, vm.interns)?.into_owned();\n                self.finditer(&text, vm.heap)\n            }\n            _ => return Err(ExcType::attribute_error(Type::RePattern, attr.as_str(vm.interns))),\n        }?;\n        Ok(CallResult::Value(result))\n    }\n}\n\nimpl HeapItem for RePattern {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.pattern.len()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // No heap references — all data is owned.\n    }\n}\n\n/// Handles `pattern.sub(repl, string, count=0)` argument extraction and dispatch.\n///\n/// Separated from the main `py_call_attr` match to keep the borrow checker happy —\n/// extracting multiple string arguments requires careful ordering of borrows.\n/// Supports `count` as either positional or keyword argument.\nfn call_pattern_sub(\n    pattern: &RePattern,\n    args: ArgValues,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Value> {\n    let (pos, kwargs) = args.into_parts();\n    defer_drop_mut!(pos, heap);\n    let kwargs = kwargs.into_iter();\n    defer_drop_mut!(kwargs, heap);\n\n    let Some(repl_val) = pos.next() else {\n        return Err(ExcType::type_error(\"Pattern.sub() missing required argument: 'repl'\"));\n    };\n    defer_drop!(repl_val, heap);\n\n    let Some(string_val) = pos.next() else {\n        return Err(ExcType::type_error(\"Pattern.sub() missing required argument: 'string'\"));\n    };\n    defer_drop!(string_val, heap);\n\n    let pos_count = pos.next();\n\n    if let Some(extra) = pos.next() {\n        extra.drop_with_heap(heap);\n        return Err(ExcType::type_error(\n            \"Pattern.sub() takes at most 3 positional arguments\",\n        ));\n    }\n\n    // Extract count from kwargs if not given positionally\n    let mut kw_count: Option<Value> = None;\n    for (key, value) in kwargs {\n        defer_drop!(key, heap);\n        let Some(keyword_name) = key.as_either_str(heap) else {\n            value.drop_with_heap(heap);\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n        let key_str = keyword_name.as_str(interns);\n        if key_str == \"count\" {\n            if pos_count.is_some() {\n                value.drop_with_heap(heap);\n                return Err(ExcType::type_error(\n                    \"Pattern.sub() got multiple values for argument 'count'\",\n                ));\n            }\n            kw_count.replace(value).drop_with_heap(heap);\n        } else {\n            value.drop_with_heap(heap);\n            return Err(ExcType::type_error(format!(\n                \"'{key_str}' is an invalid keyword argument for Pattern.sub()\"\n            )));\n        }\n    }\n\n    let count_val = pos_count.or(kw_count);\n\n    #[expect(\n        clippy::cast_sign_loss,\n        clippy::cast_possible_truncation,\n        reason = \"n is checked non-negative above\"\n    )]\n    let count = match count_val {\n        Some(Value::Int(n)) if n >= 0 => n as usize,\n        Some(Value::Bool(b)) => usize::from(b),\n        Some(Value::Int(_)) => {\n            let text = value_to_str(string_val, heap, interns)?.into_owned();\n            let s = Str::new(text);\n            return Ok(Value::Ref(heap.allocate(HeapData::Str(s))?));\n        }\n        Some(other) => {\n            let t = other.py_type(heap);\n            other.drop_with_heap(heap);\n            return Err(ExcType::type_error(format!(\"expected int for count, not {t}\")));\n        }\n        None => 0,\n    };\n\n    // Check that repl is a string — callable replacement is not supported\n    if !repl_val.is_str(heap) {\n        return Err(ExcType::type_error(\n            \"callable replacement is not yet supported in re.sub()\",\n        ));\n    }\n    let repl = value_to_str(repl_val, heap, interns)?.into_owned();\n    let text = value_to_str(string_val, heap, interns)?.into_owned();\n    pattern.sub(&repl, &text, count, heap)\n}\n\n/// Handles `pattern.split(string, maxsplit=0)` argument extraction and dispatch.\n///\n/// Supports `maxsplit` as either positional or keyword argument.\nfn call_pattern_split(\n    pattern: &RePattern,\n    args: ArgValues,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<Value> {\n    let (pos, kwargs) = args.into_parts();\n    defer_drop_mut!(pos, heap);\n    let kwargs = kwargs.into_iter();\n    defer_drop_mut!(kwargs, heap);\n\n    let Some(string_val) = pos.next() else {\n        return Err(ExcType::type_error(\n            \"Pattern.split() missing required argument: 'string'\",\n        ));\n    };\n    defer_drop!(string_val, heap);\n\n    let pos_maxsplit = pos.next();\n\n    if let Some(extra) = pos.next() {\n        extra.drop_with_heap(heap);\n        return Err(ExcType::type_error(\n            \"Pattern.split() takes at most 2 positional arguments\",\n        ));\n    }\n\n    let mut kw_maxsplit: Option<Value> = None;\n    for (key, value) in kwargs {\n        defer_drop!(key, heap);\n        let Some(keyword_name) = key.as_either_str(heap) else {\n            value.drop_with_heap(heap);\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n        let key_str = keyword_name.as_str(interns);\n        if key_str == \"maxsplit\" {\n            if pos_maxsplit.is_some() {\n                value.drop_with_heap(heap);\n                return Err(ExcType::type_error(\n                    \"Pattern.split() got multiple values for argument 'maxsplit'\",\n                ));\n            }\n            kw_maxsplit.replace(value).drop_with_heap(heap);\n        } else {\n            value.drop_with_heap(heap);\n            return Err(ExcType::type_error(format!(\n                \"'{key_str}' is an invalid keyword argument for Pattern.split()\"\n            )));\n        }\n    }\n\n    let maxsplit = extract_maxsplit(pos_maxsplit.or(kw_maxsplit), heap)?;\n    let text = value_to_str(string_val, heap, interns)?.into_owned();\n    pattern.split(&text, maxsplit, heap)\n}\n\n/// Extracts a `maxsplit` value from an optional `Value`.\n///\n/// Returns 0 if not provided. Negative values are treated as 0 (split all).\nfn extract_maxsplit(val: Option<Value>, heap: &mut Heap<impl ResourceTracker>) -> RunResult<usize> {\n    match val {\n        None => Ok(0),\n        Some(Value::Int(n)) if n <= 0 => Ok(0),\n        #[expect(\n            clippy::cast_sign_loss,\n            clippy::cast_possible_truncation,\n            reason = \"n is checked positive above\"\n        )]\n        Some(Value::Int(n)) => Ok(n as usize),\n        Some(Value::Bool(b)) => Ok(usize::from(b)),\n        Some(other) => {\n            let t = other.py_type(heap);\n            other.drop_with_heap(heap);\n            Err(ExcType::type_error(format!(\"expected int for maxsplit, not {t}\")))\n        }\n    }\n}\n\n/// Compiles a Python regex pattern string with flags into a Rust `Regex`.\n///\n/// Translates Python flag constants into inline regex flag prefixes:\n/// - `re.IGNORECASE` (2) → `(?i)` prefix\n/// - `re.MULTILINE` (8) → `(?m)` prefix\n/// - `re.DOTALL` (16) → `(?s)` prefix\n///\n/// # Errors\n///\n/// Returns `re.PatternError(...)` if the pattern is invalid.\npub(crate) fn compile_regex(pattern: &str, flags: u16) -> RunResult<Regex> {\n    let mut prefix = String::new();\n    if flags & IGNORECASE != 0 {\n        prefix.push('i');\n    }\n    if flags & MULTILINE != 0 {\n        prefix.push('m');\n    }\n    if flags & DOTALL != 0 {\n        prefix.push('s');\n    }\n    // Note: re.ASCII (256) is accepted but has no effect on the regex compilation.\n    // `fancy_regex` doesn't support `(?-u)` to disable Unicode mode, so `\\w`, `\\d`, `\\s`\n    // always match Unicode characters. This is a known limitation — Python 3 defaults to\n    // Unicode mode anyway, so the behavioral difference only matters for non-ASCII input.\n\n    let full_pattern = if prefix.is_empty() {\n        pattern.to_owned()\n    } else {\n        format!(\"(?{prefix}){pattern}\")\n    };\n\n    Regex::new(&full_pattern).map_err(ExcType::re_pattern_error)\n}\n\n/// Translates Python-style replacement backreferences to `fancy_regex` syntax.\n///\n/// Python uses `\\1`, `\\2`, `\\g<1>`, `\\g<name>` for backreferences in replacement strings.\n/// `fancy_regex` uses `$1`, `$2`, `${1}`, `${name}`. This function converts between them.\n///\n/// # Supported translations\n///\n/// - `\\1`–`\\9` → `$1`–`$9` (single-digit backreferences)\n/// - `\\g<N>` → `${N}` (numeric backreference with explicit syntax)\n/// - `\\g<name>` → `${name}` (named group backreference)\n/// - `\\\\` → literal backslash\n/// - `$` → `$$` (escape literal `$` so `fancy_regex` doesn't misinterpret it)\n///\n/// Returns a `Cow` to avoid allocation when no translation is needed.\n///\n/// # Limitations\n///\n/// TODO: Multi-digit backreferences like `\\10` are not fully supported. CPython\n/// greedily reads all digits after `\\` and interprets them as a group number if\n/// that group exists, otherwise falls back to octal escapes. Currently `\\10` is\n/// translated as `$1` followed by literal `0`, which is wrong when 10+ groups\n/// exist. Fixing this requires passing the pattern's capture group count into\n/// this function to disambiguate.\nfn translate_replacement(repl: &str) -> Cow<'_, str> {\n    // Fast path: no backslashes and no literal `$` means nothing to translate or escape.\n    if !repl.contains('\\\\') && !repl.contains('$') {\n        return Cow::Borrowed(repl);\n    }\n\n    let mut result = String::with_capacity(repl.len());\n    let mut chars = repl.chars().peekable();\n\n    while let Some(c) = chars.next() {\n        if c == '\\\\' {\n            match chars.peek() {\n                Some(&d) if d.is_ascii_digit() => {\n                    // TODO: This only handles single-digit backrefs (\\1–\\9).\n                    // Multi-digit like \\10 should be ${10} when group 10 exists,\n                    // but that requires knowing the group count. See docstring.\n                    result.push('$');\n                    result.push(d);\n                    chars.next();\n                }\n                Some(&'g') => {\n                    chars.next(); // consume 'g'\n                    translate_g_backref(&mut chars, &mut result);\n                }\n                Some(&'\\\\') => {\n                    result.push('\\\\');\n                    chars.next();\n                }\n                _ => {\n                    result.push('\\\\');\n                }\n            }\n        } else if c == '$' {\n            // Escape literal `$` as `$$` so `fancy_regex` doesn't interpret `$1` etc.\n            // as backreferences.\n            result.push('$');\n            result.push('$');\n        } else {\n            result.push(c);\n        }\n    }\n\n    Cow::Owned(result)\n}\n\n/// Translates a `\\g<...>` backreference to `fancy_regex` `${...}` syntax.\n///\n/// Called after `\\g` has been consumed. Reads `<name_or_number>` from the iterator\n/// and writes `${name_or_number}` to the result. If the syntax is malformed\n/// (missing `<` or `>`), the literal characters are written through unchanged.\nfn translate_g_backref(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, result: &mut String) {\n    if chars.peek() != Some(&'<') {\n        // Not \\g<...>, just literal \\g\n        result.push('\\\\');\n        result.push('g');\n        return;\n    }\n    chars.next(); // consume '<'\n\n    // Collect everything until '>'\n    let mut name = String::new();\n    loop {\n        match chars.next() {\n            Some('>') => break,\n            Some(ch) => name.push(ch),\n            None => {\n                // Unterminated \\g<... — emit literally\n                result.push('\\\\');\n                result.push('g');\n                result.push('<');\n                result.push_str(&name);\n                return;\n            }\n        }\n    }\n\n    // Write as ${name_or_number} for fancy_regex\n    result.push('$');\n    result.push('{');\n    result.push_str(&name);\n    result.push('}');\n}\n\n/// Extracts a string from a `Value`, supporting both interned and heap strings.\n///\n/// Returns a `Cow<str>` to avoid unnecessary copies for interned strings.\npub(crate) fn value_to_str<'a>(\n    val: &'a Value,\n    heap: &'a Heap<impl ResourceTracker>,\n    interns: &'a Interns,\n) -> RunResult<Cow<'a, str>> {\n    match val {\n        Value::InternString(string_id) => Ok(Cow::Borrowed(interns.get_str(*string_id))),\n        Value::Ref(heap_id) => match heap.get(*heap_id) {\n            HeapData::Str(s) => Ok(Cow::Borrowed(s.as_str())),\n            other => Err(ExcType::type_error(format!(\n                \"expected string, not {}\",\n                other.py_type(heap)\n            ))),\n        },\n        _ => Err(ExcType::type_error(format!(\n            \"expected string, not {}\",\n            val.py_type(heap)\n        ))),\n    }\n}\n\nimpl Serialize for RePattern {\n    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        // Serialize only pattern string and flags; regex is recompiled on deserialize.\n        (&self.pattern, self.flags).serialize(serializer)\n    }\n}\n\nimpl<'de> Deserialize<'de> for RePattern {\n    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        let (pattern, flags): (String, u16) = Deserialize::deserialize(deserializer)?;\n        Self::compile(pattern, flags).map_err(|e| serde::de::Error::custom(format!(\"{e:?}\")))\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/set.rs",
    "content": "use std::fmt::Write;\n\nuse ahash::AHashSet;\nuse hashbrown::HashTable;\n\nuse super::{MontyIter, PyTrait};\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult},\n    heap::{ContainsHeap, DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem},\n    intern::{Interns, StaticStrings},\n    resource::{ResourceError, ResourceTracker},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Entry in the set storage, containing a value and its cached hash.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nstruct SetEntry {\n    pub(crate) value: Value,\n    /// Cached hash for efficient lookup and reinsertion.\n    pub(crate) hash: u64,\n}\n\n/// Internal storage shared between Set and FrozenSet.\n///\n/// Uses a `HashTable<usize>` for O(1) lookups combined with a dense `Vec<SetEntry>`\n/// to preserve insertion order (consistent with Python 3.7+ dict behavior).\n/// The hash table maps value hashes to indices in the entries vector.\n#[derive(Debug, Default)]\npub(crate) struct SetStorage {\n    /// Maps hash to index in entries vector.\n    indices: HashTable<usize>,\n    /// Dense vector of entries maintaining insertion order.\n    entries: Vec<SetEntry>,\n}\n\nimpl SetStorage {\n    /// Creates a new empty set storage.\n    fn new() -> Self {\n        Self::default()\n    }\n\n    /// Creates a new set storage with pre-allocated capacity.\n    fn with_capacity(capacity: usize) -> Self {\n        Self {\n            indices: HashTable::with_capacity(capacity),\n            entries: Vec::with_capacity(capacity),\n        }\n    }\n\n    /// Creates a SetStorage from a vector of (value, hash) pairs.\n    ///\n    /// This is used to avoid borrow conflicts when we need to copy another set's\n    /// contents and then perform operations requiring mutable heap access.\n    /// The caller is responsible for handling reference counting.\n    fn from_entries(entries: Vec<(Value, u64)>) -> Self {\n        let mut storage = Self::with_capacity(entries.len());\n        for (idx, (value, hash)) in entries.into_iter().enumerate() {\n            storage.entries.push(SetEntry { value, hash });\n            storage.indices.insert_unique(hash, idx, |&i| storage.entries[i].hash);\n        }\n        storage\n    }\n\n    /// Clones entries with proper reference counting.\n    fn clone_entries(&self, heap: &impl ContainsHeap) -> Vec<(Value, u64)> {\n        self.entries\n            .iter()\n            .map(|e| (e.value.clone_with_heap(heap), e.hash))\n            .collect()\n    }\n\n    /// Returns the number of elements in the set.\n    fn len(&self) -> usize {\n        self.entries.len()\n    }\n\n    /// Returns true if the set is empty.\n    fn is_empty(&self) -> bool {\n        self.entries.is_empty()\n    }\n\n    /// Returns whether this set contains any heap references (`Value::Ref`).\n    ///\n    /// Used during allocation to determine if this container could create cycles.\n    #[inline]\n    fn has_refs(&self) -> bool {\n        self.entries.iter().any(|e| matches!(e.value, Value::Ref(_)))\n    }\n\n    /// Adds an element to the set, transferring ownership.\n    ///\n    /// Returns `Ok(true)` if the element was added (not already present),\n    /// `Ok(false)` if the element was already in the set.\n    /// Returns `Err` if the element is unhashable.\n    ///\n    /// The caller transfers ownership of `value`. If the value is already in\n    /// the set, it will be dropped.\n    fn add(&mut self, value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        let hash = match value.py_hash(vm.heap, vm.interns) {\n            Ok(Some(h)) => h,\n            Ok(None) => {\n                let err = ExcType::type_error_unhashable_set_element(value.py_type(vm.heap));\n                value.drop_with_heap(vm.heap);\n                return Err(err);\n            }\n            Err(e) => {\n                value.drop_with_heap(vm.heap);\n                return Err(e.into());\n            }\n        };\n\n        // Check if value already exists.\n        let existing = self\n            .indices\n            .find(hash, |&idx| value.py_eq(&self.entries[idx].value, vm).unwrap_or(false));\n\n        if existing.is_some() {\n            // Value already in set, drop the new value\n            value.drop_with_heap(vm.heap);\n            Ok(false)\n        } else {\n            // Add new entry\n            let index = self.entries.len();\n            self.entries.push(SetEntry { value, hash });\n            self.indices.insert_unique(hash, index, |&idx| self.entries[idx].hash);\n            Ok(true)\n        }\n    }\n\n    /// Removes an element from the set.\n    ///\n    /// Returns `Ok(true)` if the element was removed, `Ok(false)` if not found.\n    /// Returns `Err` if the key is unhashable.\n    fn remove(&mut self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        let hash = value\n            .py_hash(vm.heap, vm.interns)?\n            .ok_or_else(|| ExcType::type_error_unhashable_set_element(value.py_type(vm.heap)))?;\n\n        let entry = self.indices.entry(\n            hash,\n            |&idx| value.py_eq(&self.entries[idx].value, vm).unwrap_or(false),\n            |&idx| self.entries[idx].hash,\n        );\n\n        if let hashbrown::hash_table::Entry::Occupied(occ) = entry {\n            let index = *occ.get();\n            let removed_entry = self.entries.remove(index);\n            occ.remove();\n\n            // Update indices for entries that shifted down\n            for idx in &mut self.indices {\n                if *idx > index {\n                    *idx -= 1;\n                }\n            }\n\n            // Drop the removed value\n            removed_entry.value.drop_with_heap(vm);\n            Ok(true)\n        } else {\n            Ok(false)\n        }\n    }\n\n    /// Removes an element from the set without raising an error if not found.\n    ///\n    /// Returns `Ok(())` always (unless the key is unhashable).\n    fn discard(&mut self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        self.remove(value, vm)?;\n        Ok(())\n    }\n\n    /// Removes and returns an arbitrary element from the set.\n    ///\n    /// Returns `Err(KeyError)` if the set is empty.\n    fn pop(&mut self) -> RunResult<Value> {\n        if self.entries.is_empty() {\n            return Err(ExcType::key_error_pop_empty_set());\n        }\n\n        // Remove the last entry (most efficient)\n        let entry = self.entries.pop().expect(\"checked non-empty\");\n\n        // Remove from hash table\n        self.indices\n            .find_entry(entry.hash, |&idx| idx == self.entries.len())\n            .expect(\"entry must exist\")\n            .remove();\n\n        Ok(entry.value)\n    }\n\n    /// Removes all elements from the set.\n    fn clear(&mut self, heap: &mut Heap<impl ResourceTracker>) {\n        self.entries.drain(..).drop_with_heap(heap);\n        self.indices.clear();\n    }\n\n    /// Creates a deep clone with proper reference counting.\n    fn clone_with_heap(&self, heap: &impl ContainsHeap) -> Self {\n        Self {\n            indices: self.indices.clone(),\n            entries: self\n                .entries\n                .iter()\n                .map(|entry| SetEntry {\n                    value: entry.value.clone_with_heap(heap),\n                    hash: entry.hash,\n                })\n                .collect(),\n        }\n    }\n\n    /// Checks if the set contains a value.\n    pub fn contains(&self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        let hash = value\n            .py_hash(vm.heap, vm.interns)?\n            .ok_or_else(|| ExcType::type_error_unhashable_set_element(value.py_type(vm.heap)))?;\n\n        // Set values are typically shallow (strings, ints, tuples of primitives),\n        // so recursion errors are unlikely. If one occurs, treat it as \"not equal\".\n        Ok(self\n            .indices\n            .find(hash, |&idx| value.py_eq(&self.entries[idx].value, vm).unwrap_or(false))\n            .is_some())\n    }\n\n    /// Returns an iterator over the values in the set.\n    pub(crate) fn iter(&self) -> impl Iterator<Item = &Value> {\n        self.entries.iter().map(|e| &e.value)\n    }\n\n    /// Returns the value at the given index, if valid.\n    ///\n    /// Used by MontyIter for index-based iteration.\n    pub(crate) fn value_at(&self, index: usize) -> Option<&Value> {\n        self.entries.get(index).map(|e| &e.value)\n    }\n\n    /// Collects heap IDs for reference counting cleanup.\n    fn collect_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        for entry in &mut self.entries {\n            if let Value::Ref(id) = &entry.value {\n                stack.push(*id);\n                #[cfg(feature = \"ref-count-panic\")]\n                entry.value.dec_ref_forget();\n            }\n        }\n    }\n\n    /// Compares two sets for equality.\n    fn eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.len() != other.len() {\n            return Ok(false);\n        }\n\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        // Check that every element in self is in other\n        for entry in &self.entries {\n            if !matches!(other.contains(&entry.value, vm), Ok(true)) {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    /// Returns true if this set is a subset of other.\n    fn is_subset(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        for entry in &self.entries {\n            if !other.contains(&entry.value, vm)? {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    /// Returns true if this set is a superset of other.\n    fn is_superset(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        other.is_subset(self, vm)\n    }\n\n    /// Returns true if this set has no elements in common with other.\n    fn is_disjoint(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Iterate over the smaller set for efficiency\n        let (smaller, larger) = if self.len() <= other.len() {\n            (self, other)\n        } else {\n            (other, self)\n        };\n\n        for entry in &smaller.entries {\n            if larger.contains(&entry.value, vm)? {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    /// Returns a new set containing elements in either set (union).\n    fn union(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let mut result_guard = HeapGuard::new(self.clone_with_heap(vm), vm);\n        let (result, vm) = result_guard.as_parts_mut();\n        for entry in &other.entries {\n            let value = entry.value.clone_with_heap(vm);\n            result.add(value, vm)?;\n        }\n        Ok(result_guard.into_inner())\n    }\n\n    /// Returns a new set containing elements in both sets (intersection).\n    fn intersection(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let mut result_guard = HeapGuard::new(Self::new(), vm);\n        let (result, vm) = result_guard.as_parts_mut();\n        // Iterate over the smaller set for efficiency\n        let (smaller, larger) = if self.len() <= other.len() {\n            (self, other)\n        } else {\n            (other, self)\n        };\n\n        for entry in &smaller.entries {\n            if larger.contains(&entry.value, vm)? {\n                let value = entry.value.clone_with_heap(vm);\n                result.add(value, vm)?;\n            }\n        }\n        Ok(result_guard.into_inner())\n    }\n\n    /// Returns a new set containing elements in self but not in other (difference).\n    fn difference(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let mut result_guard = HeapGuard::new(Self::new(), vm);\n        let (result, vm) = result_guard.as_parts_mut();\n        for entry in &self.entries {\n            if !other.contains(&entry.value, vm)? {\n                let value = entry.value.clone_with_heap(vm);\n                result.add(value, vm)?;\n            }\n        }\n        Ok(result_guard.into_inner())\n    }\n\n    /// Returns a new set containing elements in either set but not both (symmetric difference).\n    fn symmetric_difference(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let mut result_guard = HeapGuard::new(Self::new(), vm);\n        let (result, vm) = result_guard.as_parts_mut();\n\n        // Add elements in self but not in other\n        for entry in &self.entries {\n            if !other.contains(&entry.value, vm)? {\n                let value = entry.value.clone_with_heap(vm);\n                result.add(value, vm)?;\n            }\n        }\n\n        // Add elements in other but not in self\n        for entry in &other.entries {\n            if !self.contains(&entry.value, vm)? {\n                let value = entry.value.clone_with_heap(vm);\n                result.add(value, vm)?;\n            }\n        }\n\n        Ok(result_guard.into_inner())\n    }\n\n    /// Adds all elements from other to this set (in-place union).\n    fn update(&mut self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        for entry in &other.entries {\n            let value = entry.value.clone_with_heap(vm);\n            self.add(value, vm)?;\n        }\n        Ok(())\n    }\n\n    /// Writes the repr format to a formatter.\n    ///\n    /// For sets, outputs `{elem1, elem2, ...}` (no type prefix).\n    /// For frozensets, outputs `frozenset({elem1, elem2, ...})`.\n    fn repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n        type_name: &str,\n    ) -> std::fmt::Result {\n        if self.is_empty() {\n            return write!(f, \"{type_name}()\");\n        }\n\n        // Check depth limit before recursing\n        let Some(token) = vm.heap.incr_recursion_depth_for_repr() else {\n            return f.write_str(\"{...}\");\n        };\n        crate::defer_drop_immutable_heap!(token, vm);\n\n        // frozenset needs type prefix: frozenset({...}), but set doesn't: {...}\n        let needs_prefix = type_name != \"set\";\n        if needs_prefix {\n            write!(f, \"{type_name}(\")?;\n        }\n\n        f.write_char('{')?;\n        let mut first = true;\n        for entry in &self.entries {\n            if !first {\n                if vm.heap.check_time().is_err() {\n                    f.write_str(\", ...[timeout]\")?;\n                    break;\n                }\n                f.write_str(\", \")?;\n            }\n            first = false;\n            entry.value.py_repr_fmt(f, vm, heap_ids)?;\n        }\n        f.write_char('}')?;\n\n        if needs_prefix {\n            f.write_char(')')?;\n        }\n\n        Ok(())\n    }\n\n    /// Estimates the memory size of this storage.\n    fn estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.len() * std::mem::size_of::<SetEntry>()\n    }\n}\n\n/// Python set type - mutable, unordered collection of unique hashable elements.\n///\n/// Sets support standard operations like add, remove, discard, pop, clear, as well\n/// as set algebra operations like union, intersection, difference, and symmetric\n/// difference.\n///\n/// # Reference Counting\n/// When values are added, their reference counts are NOT incremented by the set -\n/// the caller transfers ownership. When values are removed or the set is cleared,\n/// their reference counts are decremented.\n#[derive(Debug, Default)]\npub(crate) struct Set(SetStorage);\n\nimpl Set {\n    /// Creates a new empty set.\n    #[must_use]\n    pub fn new() -> Self {\n        Self(SetStorage::new())\n    }\n\n    /// Creates a set with pre-allocated capacity.\n    #[must_use]\n    pub fn with_capacity(capacity: usize) -> Self {\n        Self(SetStorage::with_capacity(capacity))\n    }\n\n    /// Returns the number of elements in the set.\n    #[must_use]\n    pub fn len(&self) -> usize {\n        self.0.len()\n    }\n\n    /// Returns true if the set is empty.\n    #[must_use]\n    pub fn is_empty(&self) -> bool {\n        self.0.is_empty()\n    }\n\n    /// Returns whether this set contains any heap references (`Value::Ref`).\n    ///\n    /// Used during allocation to determine if this container could create cycles.\n    #[inline]\n    #[must_use]\n    pub fn has_refs(&self) -> bool {\n        self.0.has_refs()\n    }\n\n    /// Adds an element to the set, transferring ownership.\n    ///\n    /// Returns `Ok(true)` if added, `Ok(false)` if already present.\n    pub fn add(&mut self, value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        self.0.add(value, vm)\n    }\n\n    /// Removes an element from the set.\n    ///\n    /// Returns `Err(KeyError)` if the element is not present.\n    pub fn remove(&mut self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        if self.0.remove(value, vm)? {\n            Ok(())\n        } else {\n            Err(ExcType::key_error(value, vm))\n        }\n    }\n\n    /// Removes an element from the set if present.\n    ///\n    /// Does not raise an error if the element is not found.\n    pub fn discard(&mut self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        self.0.discard(value, vm)\n    }\n\n    /// Removes and returns an arbitrary element from the set.\n    ///\n    /// Returns `Err(KeyError)` if the set is empty.\n    pub fn pop(&mut self) -> RunResult<Value> {\n        self.0.pop()\n    }\n\n    /// Removes all elements from the set.\n    pub fn clear(&mut self, heap: &mut Heap<impl ResourceTracker>) {\n        self.0.clear(heap);\n    }\n\n    /// Returns a shallow copy of the set.\n    #[must_use]\n    pub fn copy(&self, heap: &mut Heap<impl ResourceTracker>) -> Self {\n        Self(self.0.clone_with_heap(heap))\n    }\n\n    /// Checks if the set contains a value.\n    pub fn contains(&self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        self.0.contains(value, vm)\n    }\n\n    /// Returns the internal storage (for set operations between Set and FrozenSet).\n    pub(crate) fn storage(&self) -> &SetStorage {\n        &self.0\n    }\n\n    /// Returns an iterator over the set's elements in insertion order.\n    ///\n    /// This is primarily used by other runtime helpers that need to implement\n    /// set-like protocols while still preserving Monty's single canonical set\n    /// storage implementation.\n    pub(crate) fn iter(&self) -> impl Iterator<Item = &Value> {\n        self.0.iter()\n    }\n\n    /// Creates a set from the `set()` constructor call.\n    ///\n    /// - `set()` with no args returns an empty set\n    /// - `set(iterable)` creates a set from any iterable (list, tuple, set, dict, range, str, bytes)\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let value = args.get_zero_one_arg(\"set\", vm.heap)?;\n        let set = match value {\n            None => Self::new(),\n            Some(v) => Self::from_iterable(v, vm)?,\n        };\n        let heap_id = vm.heap.allocate(HeapData::Set(set))?;\n        Ok(Value::Ref(heap_id))\n    }\n\n    /// Creates a set from a MontyIter, adding elements one by one.\n    ///\n    /// Unlike list/tuple which can just collect into a Vec, sets need to add\n    /// each element individually to handle duplicates and compute hashes.\n    fn from_iterator(iter: MontyIter, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        defer_drop_mut!(iter, vm);\n        let mut set = Self::with_capacity(iter.size_hint(vm.heap));\n        while let Some(item) = iter.for_next(vm)? {\n            set.add(item, vm)?;\n        }\n        Ok(set)\n    }\n\n    /// Creates a set from an iterable value.\n    ///\n    /// This is a convenience method used by helper methods that need to convert\n    /// arbitrary iterables to sets. It uses `MontyIter` internally.\n    fn from_iterable(iterable: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let iter = MontyIter::new(iterable, vm)?;\n        let set = Self::from_iterator(iter, vm)?;\n        Ok(set)\n    }\n}\n\nimpl DropWithHeap for Set {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        self.0.drop_with_heap(heap);\n    }\n}\n\nimpl DropWithHeap for SetStorage {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        self.entries.drop_with_heap(heap);\n    }\n}\n\nimpl DropWithHeap for FrozenSet {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        self.0.drop_with_heap(heap);\n    }\n}\n\nimpl DropWithHeap for SetEntry {\n    fn drop_with_heap<H: ContainsHeap>(self, heap: &mut H) {\n        self.value.drop_with_heap(heap);\n    }\n}\n\nimpl PyTrait for Set {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Set\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.len())\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.len() != other.len() {\n            return Ok(false);\n        }\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        self.0.eq(&other.0, vm)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        self.0.repr_fmt(f, vm, heap_ids, \"set\")\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let value = match attr.static_string() {\n            Some(StaticStrings::Add) => {\n                let value = args.get_one_arg(\"set.add\", vm.heap)?;\n                self.add(value, vm)?;\n                Ok(Value::None)\n            }\n            Some(StaticStrings::Remove) => {\n                let value = args.get_one_arg(\"set.remove\", vm.heap)?;\n                defer_drop!(value, vm);\n                self.remove(value, vm)?;\n                Ok(Value::None)\n            }\n            Some(StaticStrings::Discard) => {\n                let value = args.get_one_arg(\"set.discard\", vm.heap)?;\n                defer_drop!(value, vm);\n                self.discard(value, vm)?;\n                Ok(Value::None)\n            }\n            Some(StaticStrings::Pop) => {\n                args.check_zero_args(\"set.pop\", vm.heap)?;\n                self.pop()\n            }\n            Some(StaticStrings::Clear) => {\n                args.check_zero_args(\"set.clear\", vm.heap)?;\n                self.clear(vm.heap);\n                Ok(Value::None)\n            }\n            Some(StaticStrings::Copy) => {\n                args.check_zero_args(\"set.copy\", vm.heap)?;\n                let copy = self.copy(vm.heap);\n                let heap_id = vm.heap.allocate(HeapData::Set(copy))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Update) => {\n                let other = args.get_one_arg(\"set.update\", vm.heap)?;\n                self.update_from_value(other, vm)?;\n                Ok(Value::None)\n            }\n            Some(StaticStrings::Union) => {\n                let other = args.get_one_arg(\"set.union\", vm.heap)?;\n                let result = self.union_from_value(other, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::Set(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Intersection) => {\n                let other = args.get_one_arg(\"set.intersection\", vm.heap)?;\n                let result = self.intersection_from_value(other, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::Set(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Difference) => {\n                let other = args.get_one_arg(\"set.difference\", vm.heap)?;\n                let result = self.difference_from_value(other, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::Set(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::SymmetricDifference) => {\n                let other = args.get_one_arg(\"set.symmetric_difference\", vm.heap)?;\n                let result = self.symmetric_difference_from_value(other, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::Set(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Issubset) => {\n                let other = args.get_one_arg(\"set.issubset\", vm.heap)?;\n                defer_drop!(other, vm);\n                Ok(Value::Bool(self.issubset_from_value(other, vm)?))\n            }\n            Some(StaticStrings::Issuperset) => {\n                let other = args.get_one_arg(\"set.issuperset\", vm.heap)?;\n                defer_drop!(other, vm);\n                Ok(Value::Bool(self.issuperset_from_value(other, vm)?))\n            }\n            Some(StaticStrings::Isdisjoint) => {\n                let other = args.get_one_arg(\"set.isdisjoint\", vm.heap)?;\n                defer_drop!(other, vm);\n                Ok(Value::Bool(self.isdisjoint_from_value(other, vm)?))\n            }\n            _ => {\n                args.drop_with_heap(vm);\n                return Err(ExcType::attribute_error(Type::Set, attr.as_str(vm.interns)));\n            }\n        };\n        value.map(CallResult::Value)\n    }\n\n    fn py_sub(\n        &self,\n        _other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, crate::resource::ResourceError> {\n        // This is called from heap.rs with two Sets\n        // We need interns for contains check, but py_sub doesn't have it\n        // This is a limitation - we'll need to handle this differently\n        // For now, return None to indicate not supported via this path\n        Ok(None)\n    }\n}\n\n/// Pure set/frozenset binary operators shared by both concrete container types.\n#[derive(Debug, Clone, Copy)]\npub(crate) enum SetBinaryOp {\n    And,\n    Or,\n    Xor,\n    Sub,\n}\n\n/// Helper methods for set operations with arbitrary iterables.\nimpl Set {\n    /// Implements operator-form set algebra, which only accepts set/frozenset operands.\n    ///\n    /// Unlike method forms such as `set.union(iterable)`, the binary operators\n    /// `& | ^ -` are intentionally strict and return `None` for operands outside\n    /// the set-like values CPython accepts here (`set`, `frozenset`,\n    /// `dict_keys`, and `dict_items`) so the VM can raise the standard\n    /// unsupported-operands `TypeError`.\n    pub(crate) fn binary_op_value(\n        &self,\n        other: &Value,\n        op: SetBinaryOp,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Option<Self>> {\n        let Some(other_storage) = get_storage_from_set_operand(other, vm)? else {\n            return Ok(None);\n        };\n        defer_drop!(other_storage, vm);\n\n        let result = match op {\n            SetBinaryOp::And => Self(self.0.intersection(other_storage, vm)?),\n            SetBinaryOp::Or => Self(self.0.union(other_storage, vm)?),\n            SetBinaryOp::Xor => Self(self.0.symmetric_difference(other_storage, vm)?),\n            SetBinaryOp::Sub => Self(self.0.difference(other_storage, vm)?),\n        };\n        Ok(Some(result))\n    }\n\n    /// Updates this set with elements from an iterable value.\n    fn update_from_value(&mut self, other: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        let heap = &mut *vm.heap;\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match &other {\n            Value::Ref(id) => match heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            other.drop_with_heap(heap);\n            for (value, _hash) in entries {\n                self.add(value, vm)?;\n            }\n            return Ok(());\n        }\n\n        // Fall back to creating a temporary set from the iterable\n        let temp_set = Self::from_iterable(other, vm)?;\n        defer_drop!(temp_set, vm);\n        self.0.update(&temp_set.0, vm)?;\n        Ok(())\n    }\n\n    /// Returns a new set with elements from both this set and an iterable.\n    fn union_from_value(&self, other: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let other_storage = Self::get_storage_from_value(other, vm)?;\n        defer_drop!(other_storage, vm);\n        let result_storage = self.0.union(other_storage, vm)?;\n        Ok(Self(result_storage))\n    }\n\n    /// Returns a new set with elements common to both this set and an iterable.\n    fn intersection_from_value(&self, other: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let other_storage = Self::get_storage_from_value(other, vm)?;\n        defer_drop!(other_storage, vm);\n        let result_storage = self.0.intersection(other_storage, vm)?;\n        Ok(Self(result_storage))\n    }\n\n    /// Returns a new set with elements in this set but not in an iterable.\n    fn difference_from_value(&self, other: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let other_storage = Self::get_storage_from_value(other, vm)?;\n        defer_drop!(other_storage, vm);\n        let result_storage = self.0.difference(other_storage, vm)?;\n        Ok(Self(result_storage))\n    }\n\n    /// Returns a new set with elements in either set but not both.\n    fn symmetric_difference_from_value(\n        &self,\n        other: Value,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Self> {\n        let other_storage = Self::get_storage_from_value(other, vm)?;\n        defer_drop!(other_storage, vm);\n        let result_storage = self.0.symmetric_difference(other_storage, vm)?;\n        Ok(Self(result_storage))\n    }\n\n    /// Checks if this set is a subset of an iterable.\n    fn issubset_from_value(&self, other: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match other {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            let other_storage = SetStorage::from_entries(entries);\n            defer_drop!(other_storage, vm);\n            return self.0.is_subset(other_storage, vm);\n        }\n\n        // Handle all other iterables (list, tuple, range, str, bytes, dict, etc.)\n        let temp = Self::from_iterable(other.clone_with_heap(vm), vm)?;\n        defer_drop!(temp, vm);\n        self.0.is_subset(&temp.0, vm)\n    }\n\n    /// Checks if this set is a superset of an iterable.\n    fn issuperset_from_value(&self, other: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match other {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            let other_storage = SetStorage::from_entries(entries);\n            defer_drop!(other_storage, vm);\n            return self.0.is_superset(other_storage, vm);\n        }\n\n        // Handle all other iterables (list, tuple, range, str, bytes, dict, etc.)\n        let temp = Self::from_iterable(other.clone_with_heap(vm), vm)?;\n        defer_drop!(temp, vm);\n        self.0.is_superset(&temp.0, vm)\n    }\n\n    /// Checks if this set has no elements in common with an iterable.\n    fn isdisjoint_from_value(&self, other: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match other {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            let other_storage = SetStorage::from_entries(entries);\n            defer_drop!(other_storage, vm);\n            return self.0.is_disjoint(other_storage, vm);\n        }\n\n        // Handle all other iterables (list, tuple, range, str, bytes, dict, etc.)\n        let temp = Self::from_iterable(other.clone_with_heap(vm), vm)?;\n        defer_drop!(temp, vm);\n        self.0.is_disjoint(&temp.0, vm)\n    }\n\n    /// Helper to get SetStorage from a Value (either directly or by conversion).\n    fn get_storage_from_value(value: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<SetStorage> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match &value {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(set) => Some(set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(set) => Some(set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            value.drop_with_heap(vm);\n            return Ok(SetStorage::from_entries(entries));\n        }\n\n        // Convert iterable to set\n        let temp_set = Self::from_iterable(value, vm)?;\n        Ok(temp_set.0)\n    }\n}\n\nimpl HeapItem for Set {\n    fn py_estimate_size(&self) -> usize {\n        self.0.estimate_size()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.0.collect_dec_ref_ids(stack);\n    }\n}\n\n/// Python frozenset type - immutable, unordered collection of unique hashable elements.\n///\n/// FrozenSets support the same set algebra operations as sets (union, intersection,\n/// difference, symmetric difference) but are immutable and therefore hashable.\n///\n/// # Hashability\n/// Unlike mutable sets, frozensets can be used as dict keys or set elements because\n/// they are immutable. The hash is computed as the XOR of element hashes (order-independent).\n#[derive(Debug, Default)]\npub(crate) struct FrozenSet(SetStorage);\n\nimpl FrozenSet {\n    /// Creates a new empty frozenset.\n    #[must_use]\n    pub fn new() -> Self {\n        Self(SetStorage::new())\n    }\n\n    /// Returns the number of elements in the frozenset.\n    #[must_use]\n    pub fn len(&self) -> usize {\n        self.0.len()\n    }\n\n    /// Returns true if the frozenset is empty.\n    #[must_use]\n    pub fn is_empty(&self) -> bool {\n        self.0.is_empty()\n    }\n\n    /// Returns whether this frozenset contains any heap references (`Value::Ref`).\n    ///\n    /// Used during allocation to determine if this container could create cycles.\n    #[inline]\n    #[must_use]\n    pub fn has_refs(&self) -> bool {\n        self.0.has_refs()\n    }\n\n    /// Returns a shallow copy of the frozenset.\n    #[must_use]\n    pub fn copy(&self, heap: &mut Heap<impl ResourceTracker>) -> Self {\n        Self(self.0.clone_with_heap(heap))\n    }\n\n    /// Returns the internal storage.\n    pub(crate) fn storage(&self) -> &SetStorage {\n        &self.0\n    }\n\n    /// Checks if the frozenset contains a value.\n    pub fn contains(&self, value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        self.0.contains(value, vm)\n    }\n\n    /// Computes the hash of this frozenset.\n    ///\n    /// The hash is the XOR of all element hashes, making it order-independent.\n    /// Checks recursion depth before recursing into element hashes.\n    pub fn compute_hash(\n        &self,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> Result<Option<u64>, ResourceError> {\n        let token = heap.incr_recursion_depth()?;\n        defer_drop!(token, heap);\n        let mut hash: u64 = 0;\n        for entry in &self.0.entries {\n            // All elements must be hashable (enforced at construction)\n            match entry.value.py_hash(heap, interns)? {\n                Some(h) => hash ^= h,\n                None => return Ok(None),\n            }\n        }\n        Ok(Some(hash))\n    }\n\n    /// Creates a frozenset from a Set, consuming the Set's storage.\n    ///\n    /// This is used when we need to convert a mutable set to an immutable frozenset\n    /// without cloning.\n    pub fn from_set(set: Set) -> Self {\n        Self(set.0)\n    }\n\n    /// Creates a frozenset from the `frozenset()` constructor call.\n    ///\n    /// - `frozenset()` with no args returns an empty frozenset\n    /// - `frozenset(iterable)` creates a frozenset from any iterable (list, tuple, set, dict, range, str, bytes)\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let value = args.get_zero_one_arg(\"frozenset\", vm.heap)?;\n        let frozenset = match value {\n            None => Self::new(),\n            Some(v) => Self::from_set(Set::from_iterable(v, vm)?),\n        };\n        let heap_id = vm.heap.allocate(HeapData::FrozenSet(frozenset))?;\n        Ok(Value::Ref(heap_id))\n    }\n\n    /// Returns a new frozenset with elements from both this and another set.\n    pub(crate) fn union(&self, other: &SetStorage, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        Ok(Self(self.0.union(other, vm)?))\n    }\n\n    /// Returns a new frozenset with elements common to both sets.\n    pub(crate) fn intersection(\n        &self,\n        other: &SetStorage,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Self> {\n        Ok(Self(self.0.intersection(other, vm)?))\n    }\n\n    /// Returns a new frozenset with elements in this set but not in other.\n    pub(crate) fn difference(&self, other: &SetStorage, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        Ok(Self(self.0.difference(other, vm)?))\n    }\n\n    /// Returns a new frozenset with elements in either set but not both.\n    pub(crate) fn symmetric_difference(\n        &self,\n        other: &SetStorage,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Self> {\n        Ok(Self(self.0.symmetric_difference(other, vm)?))\n    }\n}\n\nimpl PyTrait for FrozenSet {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::FrozenSet\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.len())\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.len() != other.len() {\n            return Ok(false);\n        }\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        self.0.eq(&other.0, vm)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        self.0.repr_fmt(f, vm, heap_ids, \"frozenset\")\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let heap = &mut *vm.heap;\n        let interns = vm.interns;\n        let value = match attr.static_string() {\n            Some(StaticStrings::Copy) => {\n                args.check_zero_args(\"frozenset.copy\", heap)?;\n                let copy = self.copy(heap);\n                let heap_id = heap.allocate(HeapData::FrozenSet(copy))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Union) => {\n                let other = args.get_one_arg(\"frozenset.union\", heap)?;\n                let other_storage = Set::get_storage_from_value(other, vm)?;\n                defer_drop!(other_storage, vm);\n                let result = self.union(other_storage, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::FrozenSet(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Intersection) => {\n                let other = args.get_one_arg(\"frozenset.intersection\", heap)?;\n                let other_storage = Set::get_storage_from_value(other, vm)?;\n                defer_drop!(other_storage, vm);\n                let result = self.intersection(other_storage, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::FrozenSet(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Difference) => {\n                let other = args.get_one_arg(\"frozenset.difference\", heap)?;\n                let other_storage = Set::get_storage_from_value(other, vm)?;\n                defer_drop!(other_storage, vm);\n                let result = self.difference(other_storage, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::FrozenSet(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::SymmetricDifference) => {\n                let other = args.get_one_arg(\"frozenset.symmetric_difference\", heap)?;\n                let other_storage = Set::get_storage_from_value(other, vm)?;\n                defer_drop!(other_storage, vm);\n                let result = self.symmetric_difference(other_storage, vm)?;\n                let heap_id = vm.heap.allocate(HeapData::FrozenSet(result))?;\n                Ok(Value::Ref(heap_id))\n            }\n            Some(StaticStrings::Issubset) => {\n                let other = args.get_one_arg(\"frozenset.issubset\", heap)?;\n                defer_drop!(other, vm);\n                Ok(Value::Bool(self.issubset_from_value(other, vm)?))\n            }\n            Some(StaticStrings::Issuperset) => {\n                let other = args.get_one_arg(\"frozenset.issuperset\", heap)?;\n                defer_drop!(other, vm);\n                Ok(Value::Bool(self.issuperset_from_value(other, vm)?))\n            }\n            Some(StaticStrings::Isdisjoint) => {\n                let other = args.get_one_arg(\"frozenset.isdisjoint\", heap)?;\n                defer_drop!(other, vm);\n                Ok(Value::Bool(self.isdisjoint_from_value(other, vm)?))\n            }\n            _ => {\n                args.drop_with_heap(heap);\n                return Err(ExcType::attribute_error(Type::FrozenSet, attr.as_str(interns)));\n            }\n        };\n        value.map(CallResult::Value)\n    }\n\n    fn py_sub(\n        &self,\n        _other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, crate::resource::ResourceError> {\n        // Same limitation as Set - needs interns\n        Ok(None)\n    }\n}\n\nimpl HeapItem for FrozenSet {\n    fn py_estimate_size(&self) -> usize {\n        self.0.estimate_size()\n    }\n\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        self.0.collect_dec_ref_ids(stack);\n    }\n}\n\n/// Helper methods for frozenset operations with arbitrary iterables.\nimpl FrozenSet {\n    /// Implements operator-form set algebra, which only accepts set/frozenset operands.\n    ///\n    /// CPython returns the type of the left operand for pure set/frozenset binary\n    /// operators, so this helper keeps the result as `frozenset` even when the\n    /// right operand is a mutable `set`. Like `set`, the accepted right-hand\n    /// side includes CPython's set-like dict views.\n    pub(crate) fn binary_op_value(\n        &self,\n        other: &Value,\n        op: SetBinaryOp,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<Option<Self>> {\n        let Some(other_storage) = get_storage_from_set_operand(other, vm)? else {\n            return Ok(None);\n        };\n        defer_drop!(other_storage, vm);\n\n        let result = match op {\n            SetBinaryOp::And => Self(self.0.intersection(other_storage, vm)?),\n            SetBinaryOp::Or => Self(self.0.union(other_storage, vm)?),\n            SetBinaryOp::Xor => Self(self.0.symmetric_difference(other_storage, vm)?),\n            SetBinaryOp::Sub => Self(self.0.difference(other_storage, vm)?),\n        };\n        Ok(Some(result))\n    }\n\n    /// Checks if this frozenset is a subset of an iterable.\n    fn issubset_from_value(&self, other: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match other {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            // Build temporary storage and check\n            let other_storage = SetStorage::from_entries(entries);\n            defer_drop!(other_storage, vm);\n            return self.0.is_subset(other_storage, vm);\n        }\n\n        // Handle all other iterables (list, tuple, range, str, bytes, dict, etc.)\n        let temp = Set::from_iterable(other.clone_with_heap(vm), vm)?;\n        defer_drop!(temp, vm);\n        self.0.is_subset(&temp.0, vm)\n    }\n\n    /// Checks if this frozenset is a superset of an iterable.\n    fn issuperset_from_value(&self, other: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match other {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            // Build temporary storage and check\n            let other_storage = SetStorage::from_entries(entries);\n            defer_drop!(other_storage, vm);\n            return self.0.is_superset(other_storage, vm);\n        }\n\n        // Handle all other iterables (list, tuple, range, str, bytes, dict, etc.)\n        let temp = Set::from_iterable(other.clone_with_heap(vm), vm)?;\n        defer_drop!(temp, vm);\n        self.0.is_superset(&temp.0, vm)\n    }\n\n    /// Checks if this frozenset has no elements in common with an iterable.\n    fn isdisjoint_from_value(&self, other: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        // Try to get entries from a Set/FrozenSet directly\n        let entries_opt = match other {\n            Value::Ref(id) => match vm.heap.get(*id) {\n                HeapData::Set(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                HeapData::FrozenSet(other_set) => Some(other_set.0.clone_entries(vm.heap)),\n                _ => None,\n            },\n            _ => None,\n        };\n\n        if let Some(entries) = entries_opt {\n            // Build temporary storage and check\n            let other_storage = SetStorage::from_entries(entries);\n            defer_drop!(other_storage, vm);\n            return self.0.is_disjoint(other_storage, vm);\n        }\n\n        // Handle all other iterables (list, tuple, range, str, bytes, dict, etc.)\n        let temp = Set::from_iterable(other.clone_with_heap(vm), vm)?;\n        defer_drop!(temp, vm);\n        self.0.is_disjoint(&temp.0, vm)\n    }\n}\n\n/// Returns temporary set storage only for operator-valid set operands.\n///\n/// This is stricter than `Set::get_storage_from_value(...)`: operator forms\n/// only accept CPython's set-like operands (`set`, `frozenset`, `dict_keys`,\n/// and `dict_items`), while method forms accept any iterable.\nfn get_storage_from_set_operand(\n    value: &Value,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Option<SetStorage>> {\n    let Value::Ref(id) = value else {\n        return Ok(None);\n    };\n\n    match vm.heap.get(*id) {\n        HeapData::Set(set) => Ok(Some(SetStorage::from_entries(set.0.clone_entries(vm.heap)))),\n        HeapData::FrozenSet(set) => Ok(Some(SetStorage::from_entries(set.0.clone_entries(vm.heap)))),\n        // Dict views are `Copy` — matched value is not borrowed from the heap,\n        // so `to_set` can take `&mut VM` below without conflict.\n        HeapData::DictKeysView(view) => {\n            let Set(storage) = view.to_set(vm)?;\n            Ok(Some(storage))\n        }\n        HeapData::DictItemsView(view) => {\n            let Set(storage) = view.to_set(vm)?;\n            Ok(Some(storage))\n        }\n        _ => Ok(None),\n    }\n}\n\n// Custom serde implementations for SetStorage, Set, and FrozenSet.\n// Only serialize entries; rebuild the indices hash table on deserialize.\n\nimpl serde::Serialize for SetStorage {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        self.entries.serialize(serializer)\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for SetStorage {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        let entries: Vec<SetEntry> = serde::Deserialize::deserialize(deserializer)?;\n        // Rebuild the indices hash table from the entries\n        let mut indices = HashTable::with_capacity(entries.len());\n        for (idx, entry) in entries.iter().enumerate() {\n            indices.insert_unique(entry.hash, idx, |&i| entries[i].hash);\n        }\n        Ok(Self { indices, entries })\n    }\n}\n\nimpl serde::Serialize for Set {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        self.0.serialize(serializer)\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for Set {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        Ok(Self(SetStorage::deserialize(deserializer)?))\n    }\n}\n\nimpl serde::Serialize for FrozenSet {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        self.0.serialize(serializer)\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for FrozenSet {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        Ok(Self(SetStorage::deserialize(deserializer)?))\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/slice.rs",
    "content": "//! Python slice type implementation.\n//!\n//! Provides a slice object representing start:stop:step indices for sequence slicing.\n//! Each field is optional (None in Python), where None means \"use the default for that field\".\n\nuse std::fmt::Write;\n\nuse ahash::AHashSet;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{Heap, HeapData, HeapId, HeapItem},\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    types::{PyTrait, Type},\n    value::{EitherStr, Value},\n};\n\n/// Python slice object representing start:stop:step indices.\n///\n/// Each field is `Option<i64>` where `None` corresponds to Python's `None`,\n/// meaning \"use the default value for this field based on context\".\n///\n/// When indexing a sequence of length `n`:\n/// - `start` defaults to 0 (or n-1 if step < 0)\n/// - `stop` defaults to n (or -1 sentinel meaning \"before index 0\" if step < 0)\n/// - `step` defaults to 1\n///\n/// The `indices(length)` method computes concrete indices from these optional values.\n#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct Slice {\n    pub start: Option<i64>,\n    pub stop: Option<i64>,\n    pub step: Option<i64>,\n}\n\nimpl Slice {\n    /// Creates a new slice with the given start, stop, and step values.\n    #[must_use]\n    pub fn new(start: Option<i64>, stop: Option<i64>, step: Option<i64>) -> Self {\n        Self { start, stop, step }\n    }\n\n    /// Creates a slice from the `slice()` constructor call.\n    ///\n    /// Supports:\n    /// - `slice(stop)` - slice with only stop (start=None, step=None)\n    /// - `slice(start, stop)` - slice with start and stop (step=None)\n    /// - `slice(start, stop, step)` - slice with all three components\n    ///\n    /// Each argument can be None to indicate \"use default\".\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        let pos_args = args.into_pos_only(\"slice\", heap)?;\n        defer_drop!(pos_args, heap);\n\n        let slice = match pos_args.as_slice() {\n            [] => return Err(ExcType::type_error_at_least(\"slice\", 1, 0)),\n            [first_arg] => {\n                let stop = value_to_option_i64(first_arg)?;\n                Self::new(None, stop, None)\n            }\n            [first_arg, second_arg] => {\n                let start = value_to_option_i64(first_arg)?;\n                let stop = value_to_option_i64(second_arg)?;\n                Self::new(start, stop, None)\n            }\n            [first_arg, second_arg, third_arg] => {\n                let start = value_to_option_i64(first_arg)?;\n                let stop = value_to_option_i64(second_arg)?;\n                let step = value_to_option_i64(third_arg)?;\n                Self::new(start, stop, step)\n            }\n            _ => return Err(ExcType::type_error_at_most(\"slice\", 3, pos_args.len())),\n        };\n\n        Ok(Value::Ref(heap.allocate(HeapData::Slice(slice))?))\n    }\n\n    /// Computes concrete indices for a sequence of the given length.\n    ///\n    /// This implements Python's `slice.indices(length)` semantics:\n    /// - Handles negative indices (wrapping from the end)\n    /// - Clamps indices to valid range [0, length]\n    /// - Returns the step direction correctly for negative steps\n    ///\n    /// Returns `(start, stop, step)` as concrete values ready for iteration.\n    /// Returns `Err(())` if step is 0 (invalid).\n    ///\n    /// # Algorithm\n    /// For positive step:\n    /// - start defaults to 0, stop defaults to length\n    /// - Both are clamped to [0, length]\n    ///\n    /// For negative step:\n    /// - start defaults to length-1, stop defaults to -1 (before beginning)\n    /// - start is clamped to [-1, length-1], stop to [-1, length-1]\n    pub fn indices(&self, length: usize) -> Result<(usize, usize, i64), ()> {\n        let step = self.step.unwrap_or(1);\n        if step == 0 {\n            return Err(());\n        }\n\n        let len = i64::try_from(length).unwrap_or(i64::MAX);\n\n        if step > 0 {\n            // Positive step: iterate forward\n            let default_start = 0;\n            let default_stop = len;\n\n            let start = self.start.map_or(default_start, |s| normalize_index(s, len, 0, len));\n            let stop = self.stop.map_or(default_stop, |s| normalize_index(s, len, 0, len));\n\n            // Convert to usize, clamping to valid range\n            let start_usize = usize::try_from(start.max(0)).unwrap_or(0);\n            let stop_usize = usize::try_from(stop.max(0)).unwrap_or(0).min(length);\n\n            Ok((start_usize, stop_usize, step))\n        } else {\n            // Negative step: iterate backward\n            // For negative step, we need different handling\n            let default_start = len - 1;\n            let default_stop = -1; // Before the beginning\n\n            let start = self\n                .start\n                .map_or(default_start, |s| normalize_index(s, len, -1, len - 1));\n            let stop = self.stop.map_or(default_stop, |s| normalize_index(s, len, -1, len - 1));\n\n            // The start can be at most len-1\n            let start_i64 = start.min(len - 1);\n            let stop_i64 = stop; // can be -1 to mean \"go all the way to beginning\"\n\n            // If start normalizes to < 0, it means the starting position is before index 0.\n            // For negative step iteration, this produces an empty slice.\n            // Return (0, 0, step) which makes the iteration condition `0 > 0` false immediately.\n            if start_i64 < 0 {\n                return Ok((0, 0, step));\n            }\n\n            let start_usize = usize::try_from(start_i64).unwrap_or(0);\n\n            // For stop, we encode it specially: if stop is -1, it means \"stop before index 0\"\n            // We'll use length + 1 as a sentinel to indicate \"stop was None or evaluates to before 0\"\n            let stop_usize = if stop_i64 < 0 {\n                length + 1 // sentinel value meaning \"go all the way to the beginning\"\n            } else {\n                usize::try_from(stop_i64).unwrap_or(0)\n            };\n\n            Ok((start_usize, stop_usize, step))\n        }\n    }\n}\n\n/// Converts a Value to Option<i64>, treating None as None.\n///\n/// Used for slice construction from both `slice()` builtin and `[start:stop:step]` syntax.\n/// Returns Ok(None) for Value::None, Ok(Some(i)) for integers/bools,\n/// or Err(TypeError) for other types.\npub(crate) fn value_to_option_i64(value: &Value) -> RunResult<Option<i64>> {\n    match value {\n        Value::None => Ok(None),\n        Value::Int(i) => Ok(Some(*i)),\n        Value::Bool(b) => Ok(Some(i64::from(*b))),\n        _ => Err(ExcType::type_error_slice_indices()),\n    }\n}\n\n/// Normalizes a slice index for a sequence of the given length.\n///\n/// Handles negative indices (counting from end) and clamps to [lower, upper].\nfn normalize_index(index: i64, length: i64, lower: i64, upper: i64) -> i64 {\n    let normalized = if index < 0 { index + length } else { index };\n    normalized.clamp(lower, upper)\n}\n\nimpl PyTrait for Slice {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Slice\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        // Slices don't have a length in Python\n        None\n    }\n\n    fn py_eq(&self, other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Ok(self.start == other.start && self.stop == other.stop && self.step == other.step)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        // Slices are always truthy in Python\n        true\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        f.write_str(\"slice(\")?;\n        format_option_i64(f, self.start)?;\n        f.write_str(\", \")?;\n        format_option_i64(f, self.stop)?;\n        f.write_str(\", \")?;\n        format_option_i64(f, self.step)?;\n        f.write_char(')')\n    }\n\n    fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<CallResult>> {\n        // Fast path: interned strings can be matched by ID without string comparison\n        if let Some(ss) = attr.static_string() {\n            return match ss {\n                StaticStrings::Start => Ok(Some(CallResult::Value(option_i64_to_value(self.start)))),\n                StaticStrings::Stop => Ok(Some(CallResult::Value(option_i64_to_value(self.stop)))),\n                StaticStrings::Step => Ok(Some(CallResult::Value(option_i64_to_value(self.step)))),\n                _ => Ok(None),\n            };\n        }\n        // Slow path: heap-allocated strings need string comparison\n        match attr.as_str(vm.interns) {\n            \"start\" => Ok(Some(CallResult::Value(option_i64_to_value(self.start)))),\n            \"stop\" => Ok(Some(CallResult::Value(option_i64_to_value(self.stop)))),\n            \"step\" => Ok(Some(CallResult::Value(option_i64_to_value(self.step)))),\n            _ => Ok(None),\n        }\n    }\n}\n\nimpl HeapItem for Slice {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // Slice doesn't contain heap references, nothing to do\n    }\n}\n\n/// Converts an Option<i64> to a Value (None or Int).\npub(crate) fn option_i64_to_value(opt: Option<i64>) -> Value {\n    match opt {\n        Some(i) => Value::Int(i),\n        None => Value::None,\n    }\n}\n\n/// Formats an Option<i64> for repr output (None or the integer).\nfn format_option_i64(f: &mut impl Write, value: Option<i64>) -> std::fmt::Result {\n    match value {\n        Some(i) => write!(f, \"{i}\"),\n        None => f.write_str(\"None\"),\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/str.rs",
    "content": "/// Python string type, wrapping a Rust `String`.\n///\n/// This type provides Python string semantics. Currently supports basic\n/// operations like length and equality comparison.\nuse std::{borrow::Cow, fmt};\nuse std::{cmp::Ordering, fmt::Write};\n\nuse ahash::AHashSet;\nuse smallvec::smallvec;\n\nuse super::{Bytes, MontyIter, PyTrait};\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop, defer_drop_mut,\n    exception_private::{ExcType, RunResult},\n    heap::{DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem},\n    intern::{StaticStrings, StringId},\n    resource::{ResourceError, ResourceTracker, check_repeat_size, check_replace_size},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Python string value stored on the heap.\n///\n/// Wraps a Rust `String` and provides Python-compatible operations.\n/// `len()` returns the number of Unicode codepoints (characters), matching Python semantics.\n#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub(crate) struct Str(Box<str>);\n\nimpl Str {\n    /// Creates a new Str from a Rust String.\n    #[must_use]\n    pub fn new(s: String) -> Self {\n        Self(s.into())\n    }\n\n    /// Returns a reference to the inner string.\n    #[must_use]\n    pub fn as_str(&self) -> &str {\n        &self.0\n    }\n\n    /// Creates a string from the `str()` constructor call.\n    ///\n    /// - `str()` with no args returns an empty string\n    /// - `str(x)` converts x to its string representation using `py_str`\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let value = args.get_zero_one_arg(\"str\", vm.heap)?;\n        match value {\n            None => Ok(Value::InternString(StaticStrings::EmptyString.into())),\n            Some(v) => {\n                defer_drop!(v, vm);\n                let s = v.py_str(vm).into_owned();\n                allocate_string(s, vm.heap)\n            }\n        }\n    }\n\n    /// Handles slice-based indexing for strings.\n    ///\n    /// Returns a new string containing the selected characters (Unicode-aware).\n    fn getitem_slice(&self, slice: &crate::types::Slice, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n        let char_count = self.0.chars().count();\n        let (start, stop, step) = slice\n            .indices(char_count)\n            .map_err(|()| ExcType::value_error_slice_step_zero())?;\n\n        let result_str = get_str_slice(&self.0, start, stop, step);\n        let heap_id = heap.allocate(HeapData::Str(Self::from(result_str)))?;\n        Ok(Value::Ref(heap_id))\n    }\n}\n\nimpl From<String> for Str {\n    fn from(s: String) -> Self {\n        Self(s.into())\n    }\n}\n\nimpl From<&str> for Str {\n    fn from(s: &str) -> Self {\n        Self(s.into())\n    }\n}\n\nimpl From<Str> for String {\n    fn from(value: Str) -> Self {\n        value.0.into_string()\n    }\n}\n\n/// Allocates a string, using interned versions when possible.\n///\n/// Optimizations:\n/// - Empty strings return the pre-interned `StaticStrings::EmptyString`\n/// - Single ASCII characters return pre-interned ASCII strings\n/// - Other strings are allocated on the heap\n///\n/// This avoids heap allocation for common cases like results from `strip()`,\n/// `split()`, string iteration, etc.\npub fn allocate_string(s: String, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    match s.len() {\n        0 => Ok(Value::InternString(StaticStrings::EmptyString.into())),\n        1 => {\n            // Single byte means single ASCII character\n            let byte = s.as_bytes()[0];\n            Ok(Value::InternString(StringId::from_ascii(byte)))\n        }\n        _ => {\n            let heap_id = heap.allocate(HeapData::Str(Str::new(s)))?;\n            Ok(Value::Ref(heap_id))\n        }\n    }\n}\n\n/// Allocates a single character as a string value.\n///\n/// ASCII characters use pre-interned strings for efficiency.\n/// Non-ASCII characters are allocated on the heap.\n///\n/// This is used by string iteration and `chr()` builtin.\npub fn allocate_char(c: char, heap: &mut Heap<impl ResourceTracker>) -> Result<Value, ResourceError> {\n    if c.is_ascii() {\n        Ok(Value::InternString(StringId::from_ascii(c as u8)))\n    } else {\n        let heap_id = heap.allocate(HeapData::Str(Str::new(c.to_string())))?;\n        Ok(Value::Ref(heap_id))\n    }\n}\n\n/// Gets the character at a given index in a string, handling negative indices.\n///\n/// Returns `None` if the index is out of bounds. This uses a single-pass scan\n/// to avoid allocating a `Vec<char>`.\n///\n/// Negative indices count from the end: -1 is the last character.\npub fn get_char_at_index(s: &str, index: i64) -> Option<char> {\n    let char_count = s.chars().count();\n    let len = i64::try_from(char_count).ok()?;\n    let normalized = if index < 0 { index + len } else { index };\n\n    if normalized < 0 || normalized >= len {\n        return None;\n    }\n\n    let idx = usize::try_from(normalized).ok()?;\n    s.chars().nth(idx)\n}\n\n/// Extracts a slice of a string (Unicode-aware).\n///\n/// Handles both positive and negative step values. For negative step,\n/// iterates backward from start down to (but not including) stop.\n/// The `stop` parameter uses a sentinel value of `len + 1` for negative\n/// step to indicate \"go to the beginning\".\n///\n/// Note: step must be non-zero (callers should validate this via `slice.indices()`).\npub(crate) fn get_str_slice(s: &str, start: usize, stop: usize, step: i64) -> String {\n    let chars: Vec<char> = s.chars().collect();\n    let mut result = String::new();\n\n    // try_from succeeds for non-negative step; step==0 rejected upstream by slice.indices()\n    if let Ok(step_usize) = usize::try_from(step) {\n        // Positive step: iterate forward\n        let mut i = start;\n        while i < stop && i < chars.len() {\n            result.push(chars[i]);\n            i += step_usize;\n        }\n    } else {\n        // Negative step: iterate backward\n        // start is the highest index, stop is the sentinel\n        // stop > chars.len() means \"go to the beginning\"\n        let step_abs = usize::try_from(-step).expect(\"step is negative so -step is positive\");\n        let step_abs_i64 = i64::try_from(step_abs).expect(\"step magnitude fits in i64\");\n        let mut i = i64::try_from(start).expect(\"start index fits in i64\");\n        // stop > chars.len() is sentinel meaning \"go to beginning\", use -1\n        let stop_i64 = if stop > chars.len() {\n            -1\n        } else {\n            i64::try_from(stop).expect(\"stop bounded by chars.len() fits in i64\")\n        };\n\n        while let Ok(i_usize) = usize::try_from(i) {\n            if i_usize >= chars.len() || i <= stop_i64 {\n                break;\n            }\n            result.push(chars[i_usize]);\n            i -= step_abs_i64;\n        }\n    }\n\n    result\n}\n\nimpl std::ops::Deref for Str {\n    type Target = str;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl PyTrait for Str {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Str\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        // Count Unicode characters, not bytes, to match Python semantics\n        Some(self.0.chars().count())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        // Check for slice first (Value::Ref pointing to HeapData::Slice)\n        if let Value::Ref(id) = key\n            && let HeapData::Slice(slice) = heap.get(*id)\n        {\n            // Clone the slice to release the borrow on heap before calling getitem_slice\n            let slice = slice.clone();\n            return self.getitem_slice(&slice, heap);\n        }\n\n        // Extract integer index, accepting Int, Bool (True=1, False=0), and LongInt\n        let index = key.as_index(heap, Type::Str)?;\n\n        // Use single-pass indexing to avoid Vec<char> allocation\n        let c = get_char_at_index(&self.0, index).ok_or_else(ExcType::str_index_error)?;\n        Ok(allocate_char(c, heap)?)\n    }\n\n    fn py_eq(&self, other: &Self, _vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        Ok(self.0 == other.0)\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.0.is_empty()\n    }\n\n    fn py_cmp(\n        &self,\n        other: &Self,\n        _vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Ordering>, ResourceError> {\n        Ok(Some(self.0.cmp(&other.0)))\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        _vm: &VM<'_, '_, impl ResourceTracker>,\n        _heap_ids: &mut AHashSet<HeapId>,\n    ) -> fmt::Result {\n        string_repr_fmt(&self.0, f)\n    }\n\n    fn py_str(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Cow<'static, str> {\n        self.0.clone().into_string().into()\n    }\n\n    fn py_add(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, crate::resource::ResourceError> {\n        let result = format!(\"{}{}\", self.0, other.0);\n        let id = vm.heap.allocate(HeapData::Str(result.into()))?;\n        Ok(Some(Value::Ref(id)))\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        let args_guard = HeapGuard::new(args, vm.heap);\n        let Some(method) = attr.static_string() else {\n            return Err(ExcType::attribute_error(Type::Str, attr.as_str(vm.interns)));\n        };\n\n        let args = args_guard.into_inner();\n        call_str_method_impl(&self.0, method, args, vm).map(CallResult::Value)\n    }\n}\n\nimpl HeapItem for Str {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.0.len()\n    }\n\n    fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {\n        // No-op: strings don't hold Value references\n    }\n}\n\n/// Dispatches a method call on a string value by method name.\n///\n/// This is the entry point for string method calls from the VM on interned strings.\n/// Converts the `StringId` to `StaticStrings` and delegates to `call_str_method_impl`.\npub fn call_str_method(\n    s: &str,\n    method_id: StringId,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Value> {\n    let args_guard = HeapGuard::new(args, vm.heap);\n    let Some(method) = StaticStrings::from_string_id(method_id) else {\n        return Err(ExcType::attribute_error(Type::Str, vm.interns.get_str(method_id)));\n    };\n    let args = args_guard.into_inner();\n    call_str_method_impl(s, method, args, vm)\n}\n\n/// Dispatches a method call on a string value.\n///\n/// This is the unified implementation for string method calls, used by both:\n/// - `Str::py_call_attr()` for heap-allocated strings\n/// - `call_str_method()` for interned string literals from the VM\n///\n/// # Not Yet Implemented\n///\n/// The following Python string methods are not yet implemented:\n///\n/// - `format()` - Requires implementing the format spec mini-language (PEP 3101),\n///   which is complex and involves parsing format specifications like `{:>10.2f}`.\n/// - `format_map(mapping)` - Similar to `format()` but takes a mapping; depends on\n///   `format()` implementation.\n/// - `maketrans()` / `translate()` - Character translation tables; moderate complexity,\n///   requires building and applying Unicode translation maps.\n/// - `expandtabs(tabsize=8)` - Tab expansion; simple but rarely used in practice.\n/// - `isprintable()` - Checks if all characters are printable; requires accurate Unicode\n///   category data for the \"printable\" property.\nfn call_str_method_impl(\n    s: &str,\n    method: StaticStrings,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Value> {\n    match method {\n        // Simple transformations (no arguments)\n        StaticStrings::Lower => {\n            args.check_zero_args(\"str.lower\", vm.heap)?;\n            str_lower(s, vm)\n        }\n        StaticStrings::Upper => {\n            args.check_zero_args(\"str.upper\", vm.heap)?;\n            str_upper(s, vm)\n        }\n        StaticStrings::Capitalize => {\n            args.check_zero_args(\"str.capitalize\", vm.heap)?;\n            str_capitalize(s, vm)\n        }\n        StaticStrings::Title => {\n            args.check_zero_args(\"str.title\", vm.heap)?;\n            str_title(s, vm)\n        }\n        StaticStrings::Swapcase => {\n            args.check_zero_args(\"str.swapcase\", vm.heap)?;\n            str_swapcase(s, vm)\n        }\n        StaticStrings::Casefold => {\n            args.check_zero_args(\"str.casefold\", vm.heap)?;\n            str_casefold(s, vm)\n        }\n        // Predicate methods (no arguments, return bool)\n        StaticStrings::Isalpha => {\n            args.check_zero_args(\"str.isalpha\", vm.heap)?;\n            Ok(Value::Bool(str_isalpha(s)))\n        }\n        StaticStrings::Isdigit => {\n            args.check_zero_args(\"str.isdigit\", vm.heap)?;\n            Ok(Value::Bool(str_isdigit(s)))\n        }\n        StaticStrings::Isalnum => {\n            args.check_zero_args(\"str.isalnum\", vm.heap)?;\n            Ok(Value::Bool(str_isalnum(s)))\n        }\n        StaticStrings::Isnumeric => {\n            args.check_zero_args(\"str.isnumeric\", vm.heap)?;\n            Ok(Value::Bool(str_isnumeric(s)))\n        }\n        StaticStrings::Isspace => {\n            args.check_zero_args(\"str.isspace\", vm.heap)?;\n            Ok(Value::Bool(str_isspace(s)))\n        }\n        StaticStrings::Islower => {\n            args.check_zero_args(\"str.islower\", vm.heap)?;\n            Ok(Value::Bool(str_islower(s)))\n        }\n        StaticStrings::Isupper => {\n            args.check_zero_args(\"str.isupper\", vm.heap)?;\n            Ok(Value::Bool(str_isupper(s)))\n        }\n        StaticStrings::Isascii => {\n            args.check_zero_args(\"str.isascii\", vm.heap)?;\n            Ok(Value::Bool(s.is_ascii()))\n        }\n        StaticStrings::Isdecimal => {\n            args.check_zero_args(\"str.isdecimal\", vm.heap)?;\n            Ok(Value::Bool(str_isdecimal(s)))\n        }\n        // Search methods\n        StaticStrings::Find => str_find(s, args, vm),\n        StaticStrings::Rfind => str_rfind(s, args, vm),\n        StaticStrings::Index => str_index(s, args, vm),\n        StaticStrings::Rindex => str_rindex(s, args, vm),\n        StaticStrings::Count => str_count(s, args, vm),\n        StaticStrings::Startswith => str_startswith(s, args, vm),\n        StaticStrings::Endswith => str_endswith(s, args, vm),\n        // Strip/trim methods\n        StaticStrings::Strip => str_strip(s, args, vm),\n        StaticStrings::Lstrip => str_lstrip(s, args, vm),\n        StaticStrings::Rstrip => str_rstrip(s, args, vm),\n        StaticStrings::Removeprefix => str_removeprefix(s, args, vm),\n        StaticStrings::Removesuffix => str_removesuffix(s, args, vm),\n        // Split methods\n        StaticStrings::Split => str_split(s, args, vm),\n        StaticStrings::Rsplit => str_rsplit(s, args, vm),\n        StaticStrings::Splitlines => str_splitlines(s, args, vm),\n        StaticStrings::Partition => str_partition(s, args, vm),\n        StaticStrings::Rpartition => str_rpartition(s, args, vm),\n        // Replace/modify methods\n        StaticStrings::Replace => str_replace(s, args, vm),\n        StaticStrings::Center => str_center(s, args, vm),\n        StaticStrings::Ljust => str_ljust(s, args, vm),\n        StaticStrings::Rjust => str_rjust(s, args, vm),\n        StaticStrings::Zfill => str_zfill(s, args, vm),\n        // Additional methods\n        StaticStrings::Encode => str_encode(s, args, vm),\n        StaticStrings::Isidentifier => {\n            args.check_zero_args(\"str.isidentifier\", vm.heap)?;\n            Ok(Value::Bool(str_isidentifier(s)))\n        }\n        StaticStrings::Istitle => {\n            args.check_zero_args(\"str.istitle\", vm.heap)?;\n            Ok(Value::Bool(str_istitle(s)))\n        }\n        // Existing method\n        StaticStrings::Join => {\n            let iterable = args.get_one_arg(\"str.join\", vm.heap)?;\n            str_join(s, iterable, vm)\n        }\n        _ => {\n            args.drop_with_heap(vm.heap);\n            Err(ExcType::attribute_error(Type::Str, method.into()))\n        }\n    }\n}\n\n/// Implements Python's `str.join(iterable)` method.\n///\n/// Joins elements of the iterable with the separator string, returning\n/// a new heap-allocated string. Each element must be a string.\n///\n/// # Arguments\n/// * `separator` - The separator string (e.g., \",\" for comma-separated)\n/// * `iterable` - The iterable containing string elements to join\n/// * `heap` - The heap for allocation and reference counting\n/// * `interns` - The interns table for resolving interned strings\n///\n/// # Errors\n/// Returns `TypeError` if the argument is not iterable or if any element is not a string.\nfn str_join(separator: &str, iterable: Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    // Create MontyIter from the iterable, with join-specific error message\n    let Ok(iter) = MontyIter::new(iterable, vm) else {\n        return Err(ExcType::type_error_join_not_iterable());\n    };\n    defer_drop_mut!(iter, vm);\n\n    // Build result string, tracking index for error messages\n    let mut result = String::new();\n    let mut index = 0usize;\n\n    while let Some(item) = iter.for_next(vm)? {\n        defer_drop!(item, vm);\n        if index > 0 {\n            result.push_str(separator);\n        }\n\n        // Check item is a string and extract its content\n        match item {\n            Value::InternString(id) => {\n                result.push_str(vm.interns.get_str(*id));\n            }\n            Value::Ref(heap_id) => {\n                if let HeapData::Str(s) = vm.heap.get(*heap_id) {\n                    result.push_str(s.as_str());\n                } else {\n                    let t = item.py_type(vm.heap);\n                    return Err(ExcType::type_error_join_item(index, t));\n                }\n            }\n            _ => {\n                let t = item.py_type(vm.heap);\n                return Err(ExcType::type_error_join_item(index, t));\n            }\n        }\n        index += 1;\n    }\n\n    // Allocate result (uses interned empty string if result is empty)\n    allocate_string(result, vm.heap)\n}\n\n/// Writes a Python repr() string for a given string slice to a formatter.\n///\n/// Chooses between single and double quotes based on the string content:\n/// - Uses double quotes if the string contains single quotes but not double quotes\n/// - Uses single quotes by default, escaping any contained single quotes\n///\n/// Common escape sequences (backslash, newline, tab, carriage return) are always escaped.\npub fn string_repr_fmt(s: &str, f: &mut impl Write) -> fmt::Result {\n    // Check if the string contains single quotes but not double quotes\n    if s.contains('\\'') && !s.contains('\"') {\n        // Use double quotes if string contains only single quotes\n        f.write_char('\"')?;\n        for c in s.chars() {\n            match c {\n                '\\\\' => f.write_str(\"\\\\\\\\\")?,\n                '\\n' => f.write_str(\"\\\\n\")?,\n                '\\t' => f.write_str(\"\\\\t\")?,\n                '\\r' => f.write_str(\"\\\\r\")?,\n                _ => f.write_char(c)?,\n            }\n        }\n        f.write_char('\"')\n    } else {\n        // Use single quotes by default, escape any single quotes in the string\n        f.write_char('\\'')?;\n        for c in s.chars() {\n            match c {\n                '\\\\' => f.write_str(\"\\\\\\\\\")?,\n                '\\n' => f.write_str(\"\\\\n\")?,\n                '\\t' => f.write_str(\"\\\\t\")?,\n                '\\r' => f.write_str(\"\\\\r\")?,\n                '\\'' => f.write_str(\"\\\\'\")?,\n                _ => f.write_char(c)?,\n            }\n        }\n        f.write_char('\\'')\n    }\n}\n\n/// Formatter for a Python repr() string.\n#[derive(Debug)]\npub struct StringRepr<'a>(pub &'a str);\n\nimpl fmt::Display for StringRepr<'_> {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        string_repr_fmt(self.0, f)\n    }\n}\n\n// =============================================================================\n// Simple transformations (no arguments)\n// =============================================================================\n\n/// Implements Python's `str.lower()` method.\nfn str_lower(s: &str, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    allocate_string(s.to_lowercase(), vm.heap)\n}\n\n/// Implements Python's `str.upper()` method.\nfn str_upper(s: &str, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    allocate_string(s.to_uppercase(), vm.heap)\n}\n\n/// Implements Python's `str.capitalize()` method.\n///\n/// Returns a copy of the string with its first character capitalized and the rest lowercased.\nfn str_capitalize(s: &str, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let mut chars = s.chars();\n    let result = match chars.next() {\n        None => String::new(),\n        Some(first) => {\n            let mut result = first.to_uppercase().to_string();\n            for c in chars {\n                result.extend(c.to_lowercase());\n            }\n            result\n        }\n    };\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.title()` method.\n///\n/// Returns a titlecased version of the string where words start with an uppercase\n/// character and the remaining characters are lowercase.\nfn str_title(s: &str, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let mut result = String::with_capacity(s.len());\n    let mut prev_is_cased = false;\n\n    for c in s.chars() {\n        if prev_is_cased {\n            result.extend(c.to_lowercase());\n        } else {\n            result.extend(c.to_uppercase());\n        }\n        prev_is_cased = c.is_alphabetic();\n    }\n\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.swapcase()` method.\n///\n/// Returns a copy of the string with uppercase characters converted to lowercase and vice versa.\nfn str_swapcase(s: &str, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let mut result = String::with_capacity(s.len());\n\n    for c in s.chars() {\n        if c.is_uppercase() {\n            result.extend(c.to_lowercase());\n        } else if c.is_lowercase() {\n            result.extend(c.to_uppercase());\n        } else {\n            result.push(c);\n        }\n    }\n\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.casefold()` method.\n///\n/// Returns a casefolded copy of the string. Casefolding is similar to lowercasing\n/// but more aggressive because it is intended for caseless string matching.\nfn str_casefold(s: &str, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    // Rust's to_lowercase() is equivalent to Unicode casefolding for most purposes\n    allocate_string(s.to_lowercase(), vm.heap)\n}\n\n// =============================================================================\n// Predicate methods (no arguments, return bool)\n// =============================================================================\n\n/// Implements Python's `str.isalpha()` method.\n///\n/// Returns True if all characters in the string are alphabetic and there is at least one character.\nfn str_isalpha(s: &str) -> bool {\n    !s.is_empty() && s.chars().all(char::is_alphabetic)\n}\n\n/// Implements Python's `str.isdigit()` method.\n///\n/// Returns True if all characters in the string are digits and there is at least one character.\n/// In Python, digits include decimal digits (Nd) plus characters with Numeric_Type=Digit\n/// (superscripts, subscripts, circled digits, etc.).\nfn str_isdigit(s: &str) -> bool {\n    !s.is_empty() && s.chars().all(is_unicode_digit)\n}\n\n/// Implements Python's `str.isalnum()` method.\n///\n/// Returns True if all characters in the string are alphanumeric and there is at least one character.\nfn str_isalnum(s: &str) -> bool {\n    !s.is_empty() && s.chars().all(char::is_alphanumeric)\n}\n\n/// Implements Python's `str.isnumeric()` method.\n///\n/// Returns True if all characters in the string are numeric and there is at least one character.\n/// In Python, numeric includes decimal digits (Nd), letter numerals (Nl), and other numerals (No).\n/// Rust's `char::is_numeric()` checks for all of these categories.\nfn str_isnumeric(s: &str) -> bool {\n    !s.is_empty() && s.chars().all(char::is_numeric)\n}\n\n/// Implements Python's `str.isspace()` method.\n///\n/// Returns True if all characters in the string are whitespace and there is at least one character.\nfn str_isspace(s: &str) -> bool {\n    !s.is_empty() && s.chars().all(char::is_whitespace)\n}\n\n/// Implements Python's `str.islower()` method.\n///\n/// Returns True if all cased characters in the string are lowercase and there is at least one cased character.\nfn str_islower(s: &str) -> bool {\n    let mut has_cased = false;\n    for c in s.chars() {\n        if c.is_uppercase() {\n            return false;\n        }\n        if c.is_lowercase() {\n            has_cased = true;\n        }\n    }\n    has_cased\n}\n\n/// Implements Python's `str.isupper()` method.\n///\n/// Returns True if all cased characters in the string are uppercase and there is at least one cased character.\nfn str_isupper(s: &str) -> bool {\n    let mut has_cased = false;\n    for c in s.chars() {\n        if c.is_lowercase() {\n            return false;\n        }\n        if c.is_uppercase() {\n            has_cased = true;\n        }\n    }\n    has_cased\n}\n\n/// Implements Python's `str.isdecimal()` method.\n///\n/// Returns True if all characters in the string are decimal characters and there is at least one character.\n/// Decimal characters are those in Unicode category Nd (Decimal_Number) - digits that can be used\n/// to form numbers in base 10.\nfn str_isdecimal(s: &str) -> bool {\n    !s.is_empty() && s.chars().all(is_unicode_decimal)\n}\n\n/// Checks if a character is a Unicode decimal digit (Nd category).\n///\n/// This covers decimal digit ranges from various scripts including ASCII, Arabic-Indic,\n/// Devanagari, Bengali, Thai, Fullwidth, and many others.\nfn is_unicode_decimal(c: char) -> bool {\n    let cp = c as u32;\n    matches!(\n        cp,\n        // Basic Latin (ASCII digits)\n        0x0030..=0x0039\n        // Arabic-Indic digits\n        | 0x0660..=0x0669\n        // Extended Arabic-Indic digits\n        | 0x06F0..=0x06F9\n        // NKo digits\n        | 0x07C0..=0x07C9\n        // Devanagari digits\n        | 0x0966..=0x096F\n        // Bengali digits\n        | 0x09E6..=0x09EF\n        // Gurmukhi digits\n        | 0x0A66..=0x0A6F\n        // Gujarati digits\n        | 0x0AE6..=0x0AEF\n        // Oriya digits\n        | 0x0B66..=0x0B6F\n        // Tamil digits\n        | 0x0BE6..=0x0BEF\n        // Telugu digits\n        | 0x0C66..=0x0C6F\n        // Kannada digits\n        | 0x0CE6..=0x0CEF\n        // Malayalam digits\n        | 0x0D66..=0x0D6F\n        // Sinhala Lith digits\n        | 0x0DE6..=0x0DEF\n        // Thai digits\n        | 0x0E50..=0x0E59\n        // Lao digits\n        | 0x0ED0..=0x0ED9\n        // Tibetan digits\n        | 0x0F20..=0x0F29\n        // Myanmar digits\n        | 0x1040..=0x1049\n        // Myanmar Shan digits\n        | 0x1090..=0x1099\n        // Khmer digits\n        | 0x17E0..=0x17E9\n        // Mongolian digits\n        | 0x1810..=0x1819\n        // Limbu digits\n        | 0x1946..=0x194F\n        // New Tai Lue digits\n        | 0x19D0..=0x19D9\n        // Tai Tham Hora digits\n        | 0x1A80..=0x1A89\n        // Tai Tham Tham digits\n        | 0x1A90..=0x1A99\n        // Balinese digits\n        | 0x1B50..=0x1B59\n        // Sundanese digits\n        | 0x1BB0..=0x1BB9\n        // Lepcha digits\n        | 0x1C40..=0x1C49\n        // Ol Chiki digits\n        | 0x1C50..=0x1C59\n        // Vai digits\n        | 0xA620..=0xA629\n        // Saurashtra digits\n        | 0xA8D0..=0xA8D9\n        // Kayah Li digits\n        | 0xA900..=0xA909\n        // Javanese digits\n        | 0xA9D0..=0xA9D9\n        // Myanmar Tai Laing digits\n        | 0xA9F0..=0xA9F9\n        // Cham digits\n        | 0xAA50..=0xAA59\n        // Meetei Mayek digits\n        | 0xABF0..=0xABF9\n        // Fullwidth digits\n        | 0xFF10..=0xFF19\n        // Osmanya digits\n        | 0x104A0..=0x104A9\n        // Hanifi Rohingya digits\n        | 0x10D30..=0x10D39\n        // Brahmi digits\n        | 0x11066..=0x1106F\n        // Sora Sompeng digits\n        | 0x110F0..=0x110F9\n        // Chakma digits\n        | 0x11136..=0x1113F\n        // Sharada digits\n        | 0x111D0..=0x111D9\n        // Khudawadi digits\n        | 0x112F0..=0x112F9\n        // Newa digits\n        | 0x11450..=0x11459\n        // Tirhuta digits\n        | 0x114D0..=0x114D9\n        // Modi digits\n        | 0x11650..=0x11659\n        // Takri digits\n        | 0x116C0..=0x116C9\n        // Ahom digits\n        | 0x11730..=0x11739\n        // Warang Citi digits\n        | 0x118E0..=0x118E9\n        // Dives Akuru digits\n        | 0x11950..=0x11959\n        // Bhaiksuki digits\n        | 0x11C50..=0x11C59\n        // Masaram Gondi digits\n        | 0x11D50..=0x11D59\n        // Gunjala Gondi digits\n        | 0x11DA0..=0x11DA9\n        // Adlam digits\n        | 0x1E950..=0x1E959\n        // Segmented digits\n        | 0x1FBF0..=0x1FBF9\n    )\n}\n\n/// Checks if a character is a Unicode digit (isdigit).\n///\n/// This includes decimal digits (Nd) plus characters with Numeric_Type=Digit\n/// such as superscripts, subscripts, and circled digits.\nfn is_unicode_digit(c: char) -> bool {\n    // First check if it's a decimal digit\n    if is_unicode_decimal(c) {\n        return true;\n    }\n\n    let cp = c as u32;\n    matches!(\n        cp,\n        // Superscripts (², ³)\n        0x00B2..=0x00B3\n        // Superscript 1\n        | 0x00B9\n        // Superscript digits 0, 4-9\n        | 0x2070\n        | 0x2074..=0x2079\n        // Subscript digits 0-9\n        | 0x2080..=0x2089\n        // Circled digits 1-9\n        | 0x2460..=0x2468\n        // Circled digit 0\n        | 0x24EA\n        // Circled digits 10-20\n        | 0x2469..=0x2473\n        // Parenthesized digits 1-9\n        | 0x2474..=0x247C\n        // Period digits 1-9\n        | 0x2488..=0x2490\n        // Double circled digits 1-10\n        | 0x24F5..=0x24FE\n        // Dingbat circled sans-serif digits 1-10\n        | 0x2780..=0x2789\n        // Dingbat negative circled digits 1-10\n        | 0x278A..=0x2793\n        // Dingbat circled sans-serif digits 1-10\n        | 0x24FF\n        // Fullwidth digit zero (already in decimal, but include for completeness)\n        // | 0xFF10..=0xFF19  // Already covered by is_unicode_decimal\n    )\n}\n\n// =============================================================================\n// Search methods\n// =============================================================================\n\n/// Implements Python's `str.find(sub, start?, end?)` method.\n///\n/// Returns the lowest index in the string where substring sub is found within\n/// the slice s[start:end]. Returns -1 if sub is not found.\nfn str_find(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_search_args(\"str.find\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    let result = match slice.find(&sub) {\n        Some(pos) => {\n            // Convert byte offset to char offset, then add start offset\n            let char_pos = slice[..pos].chars().count();\n            i64::try_from(start + char_pos).unwrap_or(i64::MAX)\n        }\n        None => -1,\n    };\n    Ok(Value::Int(result))\n}\n\n/// Implements Python's `str.rfind(sub, start?, end?)` method.\n///\n/// Returns the highest index in the string where substring sub is found within\n/// the slice s[start:end]. Returns -1 if sub is not found.\nfn str_rfind(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_search_args(\"str.rfind\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    let result = match slice.rfind(&sub) {\n        Some(pos) => {\n            // Convert byte offset to char offset, then add start offset\n            let char_pos = slice[..pos].chars().count();\n            i64::try_from(start + char_pos).unwrap_or(i64::MAX)\n        }\n        None => -1,\n    };\n    Ok(Value::Int(result))\n}\n\n/// Implements Python's `str.index(sub, start?, end?)` method.\n///\n/// Like find(), but raises ValueError when the substring is not found.\nfn str_index(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_search_args(\"str.index\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    match slice.find(&sub) {\n        Some(pos) => {\n            let char_pos = slice[..pos].chars().count();\n            let result = i64::try_from(start + char_pos).unwrap_or(i64::MAX);\n            Ok(Value::Int(result))\n        }\n        None => Err(ExcType::value_error_substring_not_found()),\n    }\n}\n\n/// Implements Python's `str.rindex(sub, start?, end?)` method.\n///\n/// Like rfind(), but raises ValueError when the substring is not found.\nfn str_rindex(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_search_args(\"str.rindex\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    match slice.rfind(&sub) {\n        Some(pos) => {\n            let char_pos = slice[..pos].chars().count();\n            let result = i64::try_from(start + char_pos).unwrap_or(i64::MAX);\n            Ok(Value::Int(result))\n        }\n        None => Err(ExcType::value_error_substring_not_found()),\n    }\n}\n\n/// Implements Python's `str.count(sub, start?, end?)` method.\n///\n/// Returns the number of non-overlapping occurrences of substring sub in\n/// the string s[start:end].\nfn str_count(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sub, start, end) = parse_search_args(\"str.count\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    let count = if sub.is_empty() {\n        // Empty string matches between every character, plus start and end\n        slice.chars().count() + 1\n    } else {\n        slice.matches(&sub).count()\n    };\n    let result = i64::try_from(count).unwrap_or(i64::MAX);\n    Ok(Value::Int(result))\n}\n\n/// Implements Python's `str.startswith(prefix, start?, end?)` method.\n///\n/// Returns True if the string starts with the prefix, otherwise returns False.\n/// The prefix argument can be a string or a tuple of strings.\nfn str_startswith(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (prefixes, start, end) = parse_prefix_suffix_args(\"str.startswith\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    let result = prefixes.iter().any(|prefix| slice.starts_with(prefix));\n    Ok(Value::Bool(result))\n}\n\n/// Implements Python's `str.endswith(suffix, start?, end?)` method.\n///\n/// Returns True if the string ends with the suffix, otherwise returns False.\n/// The suffix argument can be a string or a tuple of strings.\nfn str_endswith(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (suffixes, start, end) = parse_prefix_suffix_args(\"str.endswith\", s, args, vm)?;\n    let slice = slice_string(s, start, end);\n    let result = suffixes.iter().any(|suffix| slice.ends_with(suffix));\n    Ok(Value::Bool(result))\n}\n\n/// Parses arguments for search methods (find, rfind, index, rindex, count, startswith, endswith).\n///\n/// Returns (substring, start, end) where start and end are character indices.\nfn parse_search_args(\n    method: &str,\n    s: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(String, usize, usize)> {\n    let pos = args.into_pos_only(method, vm.heap)?;\n    defer_drop!(pos, vm);\n\n    let str_len = s.chars().count();\n    match pos.as_slice() {\n        [sub_value] => {\n            let sub = extract_string_arg(sub_value, vm)?;\n            Ok((sub, 0, str_len))\n        }\n        [sub_value, start_value] => {\n            let sub = extract_string_arg(sub_value, vm)?;\n            let start = optional_index(start_value, 0, str_len, vm)?;\n            Ok((sub, start, str_len))\n        }\n        [sub_value, start_value, end_value] => {\n            let sub = extract_string_arg(sub_value, vm)?;\n            let start = optional_index(start_value, 0, str_len, vm)?;\n            let end = optional_index(end_value, str_len, str_len, vm)?;\n            Ok((sub, start, end))\n        }\n        [] => Err(ExcType::type_error_at_least(method, 1, 0)),\n        _ => Err(ExcType::type_error_at_most(method, 3, pos.len())),\n    }\n}\n\n/// Parses arguments for startswith/endswith methods.\n///\n/// Returns (prefixes/suffixes as Vec, start, end) where start and end are character indices.\n/// The first argument can be either a string or a tuple of strings.\nfn parse_prefix_suffix_args(\n    method: &str,\n    s: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Vec<String>, usize, usize)> {\n    let pos = args.into_pos_only(method, vm.heap)?;\n    defer_drop!(pos, vm);\n\n    let str_len = s.chars().count();\n    match pos.as_slice() {\n        [prefix_value] => {\n            let prefixes = extract_str_or_tuple_of_str(prefix_value, vm)?;\n            Ok((prefixes, 0, str_len))\n        }\n        [prefix_value, start_value] => {\n            let prefixes = extract_str_or_tuple_of_str(prefix_value, vm)?;\n            let start = optional_index(start_value, 0, str_len, vm)?;\n            Ok((prefixes, start, str_len))\n        }\n        [prefix_value, start_value, end_value] => {\n            let prefixes = extract_str_or_tuple_of_str(prefix_value, vm)?;\n            let start = optional_index(start_value, 0, str_len, vm)?;\n            let end = optional_index(end_value, str_len, str_len, vm)?;\n            Ok((prefixes, start, end))\n        }\n        [] => Err(ExcType::type_error_at_least(method, 1, 0)),\n        _ => Err(ExcType::type_error_at_most(method, 3, pos.len())),\n    }\n}\n\n/// Extracts a string or tuple of strings from a Value.\n///\n/// Returns a Vec of strings - a single-element Vec if given a string,\n/// or multiple elements if given a tuple of strings.\nfn extract_str_or_tuple_of_str(value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Vec<String>> {\n    match value {\n        Value::InternString(id) => Ok(vec![vm.interns.get_str(*id).to_owned()]),\n        Value::Ref(heap_id) => match vm.heap.get(*heap_id) {\n            HeapData::Str(s) => Ok(vec![s.as_str().to_owned()]),\n            HeapData::Tuple(tuple) => {\n                // Inline string extraction to avoid borrow conflict — vm.heap is\n                // already borrowed immutably to access the tuple's items.\n                let items = tuple.as_slice();\n                let mut strings = Vec::with_capacity(items.len());\n                for item in items {\n                    match item {\n                        Value::InternString(id) => {\n                            strings.push(vm.interns.get_str(*id).to_owned());\n                        }\n                        Value::Ref(hid) => {\n                            if let HeapData::Str(s) = vm.heap.get(*hid) {\n                                strings.push(s.as_str().to_owned());\n                            } else {\n                                return Err(ExcType::type_error(\"expected str or tuple of str\"));\n                            }\n                        }\n                        _ => return Err(ExcType::type_error(\"expected str or tuple of str\")),\n                    }\n                }\n                Ok(strings)\n            }\n            _ => Err(ExcType::type_error(\"expected str or tuple of str\")),\n        },\n        _ => Err(ExcType::type_error(\"expected str or tuple of str\")),\n    }\n}\n\n/// Extracts a string from a Value, returning an error if not a string.\nfn extract_string_arg(value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<String> {\n    match value {\n        Value::InternString(id) => Ok(vm.interns.get_str(*id).to_owned()),\n        Value::Ref(heap_id) => {\n            if let HeapData::Str(s) = vm.heap.get(*heap_id) {\n                Ok(s.as_str().to_owned())\n            } else {\n                Err(ExcType::type_error(\"expected str\"))\n            }\n        }\n        _ => Err(ExcType::type_error(\"expected str\")),\n    }\n}\n\n/// Extracts an integer from a Value, returning an error if not an integer.\nfn extract_int_arg(value: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<i64> {\n    match value {\n        Value::Int(i) => Ok(*i),\n        Value::Ref(heap_id) => {\n            if let HeapData::LongInt(li) = vm.heap.get(*heap_id) {\n                // Try to convert to i64\n                li.to_i64().ok_or_else(|| ExcType::type_error(\"integer too large\"))\n            } else {\n                Err(ExcType::type_error(\"expected int\"))\n            }\n        }\n        _ => Err(ExcType::type_error(\"expected int\")),\n    }\n}\n\n/// Normalizes a Python-style index to a valid index in range [0, len].\nfn normalize_index(index: i64, len: usize) -> usize {\n    if index < 0 {\n        // Safe cast: we've checked index is negative, so -index is positive\n        // For very large negative numbers that don't fit in usize, saturate to usize::MAX\n        let abs_index = usize::try_from(-index).unwrap_or(usize::MAX);\n        len.saturating_sub(abs_index)\n    } else {\n        // Safe cast: we've checked index is non-negative\n        // For values > usize::MAX, saturate to len\n        usize::try_from(index).unwrap_or(len).min(len)\n    }\n}\n\n/// Extracts an optional index from a `Value`, treating `None` as `default`.\n///\n/// Used by argument parsers where `None` means \"use the default index\" and\n/// any other value is interpreted as an integer and normalized against `str_len`.\nfn optional_index(\n    value: &Value,\n    default: usize,\n    str_len: usize,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<usize> {\n    if matches!(value, Value::None) {\n        Ok(default)\n    } else {\n        Ok(normalize_index(extract_int_arg(value, vm)?, str_len))\n    }\n}\n\n/// Returns a substring of s from character index start to end.\nfn slice_string(s: &str, start: usize, end: usize) -> &str {\n    if start >= end {\n        return \"\";\n    }\n\n    let mut start_byte = s.len();\n    let mut end_byte = s.len();\n\n    for (char_idx, (byte_idx, _)) in s.char_indices().enumerate() {\n        if char_idx == start {\n            start_byte = byte_idx;\n        }\n        if char_idx == end {\n            end_byte = byte_idx;\n            break;\n        }\n    }\n\n    &s[start_byte..end_byte]\n}\n\n// =============================================================================\n// Strip/trim methods\n// =============================================================================\n\n/// Implements Python's `str.strip(chars?)` method.\n///\n/// Returns a copy of the string with leading and trailing characters removed.\n/// If chars is not specified, whitespace characters are removed.\nfn str_strip(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let chars = parse_strip_arg(\"str.strip\", args, vm)?;\n    let result = match &chars {\n        Some(c) => s.trim_matches(|ch| c.contains(ch)).to_owned(),\n        None => s.trim().to_owned(),\n    };\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.lstrip(chars?)` method.\n///\n/// Returns a copy of the string with leading characters removed.\nfn str_lstrip(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let chars = parse_strip_arg(\"str.lstrip\", args, vm)?;\n    let result = match &chars {\n        Some(c) => s.trim_start_matches(|ch| c.contains(ch)).to_owned(),\n        None => s.trim_start().to_owned(),\n    };\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.rstrip(chars?)` method.\n///\n/// Returns a copy of the string with trailing characters removed.\nfn str_rstrip(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let chars = parse_strip_arg(\"str.rstrip\", args, vm)?;\n    let result = match &chars {\n        Some(c) => s.trim_end_matches(|ch| c.contains(ch)).to_owned(),\n        None => s.trim_end().to_owned(),\n    };\n    allocate_string(result, vm.heap)\n}\n\n/// Parses the optional chars argument for strip methods.\n///\n/// Accepts None as a value meaning \"use default whitespace stripping\".\nfn parse_strip_arg(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<Option<String>> {\n    let value = args.get_zero_one_arg(method, vm.heap)?;\n    match value {\n        None => Ok(None),\n        Some(Value::None) => Ok(None), // Explicit None means default whitespace\n        Some(v) => {\n            defer_drop!(v, vm);\n            let result = extract_string_arg(v, vm)?;\n            Ok(Some(result))\n        }\n    }\n}\n\n/// Implements Python's `str.removeprefix(prefix)` method.\n///\n/// If the string starts with the prefix string, return string[len(prefix):].\n/// Otherwise, return a copy of the original string.\nfn str_removeprefix(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let prefix_value = args.get_one_arg(\"str.removeprefix\", vm.heap)?;\n    defer_drop!(prefix_value, vm);\n    let prefix = extract_string_arg(prefix_value, vm)?;\n\n    let result = s.strip_prefix(&prefix).unwrap_or(s).to_owned();\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.removesuffix(suffix)` method.\n///\n/// If the string ends with the suffix string, return string[:-len(suffix)].\n/// Otherwise, return a copy of the original string.\nfn str_removesuffix(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let suffix_value = args.get_one_arg(\"str.removesuffix\", vm.heap)?;\n    defer_drop!(suffix_value, vm);\n    let suffix = extract_string_arg(suffix_value, vm)?;\n\n    let result = s.strip_suffix(&suffix).unwrap_or(s).to_owned();\n    allocate_string(result, vm.heap)\n}\n\n// =============================================================================\n// Split methods\n// =============================================================================\n\n/// Implements Python's `str.split(sep?, maxsplit?)` method.\n///\n/// Returns a list of the words in the string, using sep as the delimiter string.\nfn str_split(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sep, maxsplit) = parse_split_args(\"str.split\", args, vm)?;\n\n    let parts: Vec<&str> = match &sep {\n        Some(sep) => {\n            // Empty separator raises ValueError\n            if sep.is_empty() {\n                return Err(ExcType::value_error_empty_separator());\n            }\n            if maxsplit < 0 {\n                s.split(sep.as_str()).collect()\n            } else {\n                // Safe cast: we've checked maxsplit >= 0\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                s.splitn(max.saturating_add(1), sep.as_str()).collect()\n            }\n        }\n        None => {\n            // Split on whitespace, filtering empty strings\n            if maxsplit < 0 {\n                s.split_whitespace().collect()\n            } else {\n                // Safe cast: we've checked maxsplit >= 0\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                split_whitespace_n(s, max)\n            }\n        }\n    };\n\n    // Convert to list of strings (using interned empty string when applicable)\n    let mut list_items = Vec::with_capacity(parts.len());\n    for part in parts {\n        vm.heap.check_time()?;\n        list_items.push(allocate_string(part.to_owned(), vm.heap)?);\n    }\n\n    let list = crate::types::List::new(list_items);\n    let heap_id = vm.heap.allocate(HeapData::List(list))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Implements Python's `str.rsplit(sep?, maxsplit?)` method.\n///\n/// Returns a list of the words in the string, using sep as the delimiter string,\n/// splitting from the right.\nfn str_rsplit(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (sep, maxsplit) = parse_split_args(\"str.rsplit\", args, vm)?;\n\n    let parts: Vec<&str> = match &sep {\n        Some(sep) => {\n            // Empty separator raises ValueError\n            if sep.is_empty() {\n                return Err(ExcType::value_error_empty_separator());\n            }\n            if maxsplit < 0 {\n                s.rsplit(sep.as_str()).collect::<Vec<_>>().into_iter().rev().collect()\n            } else {\n                // Safe cast: we've checked maxsplit >= 0\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                let mut parts: Vec<_> = s.rsplitn(max.saturating_add(1), sep.as_str()).collect();\n                parts.reverse();\n                parts\n            }\n        }\n        None => {\n            // Split on whitespace from right\n            if maxsplit < 0 {\n                s.split_whitespace().collect()\n            } else {\n                // Safe cast: we've checked maxsplit >= 0\n                let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);\n                rsplit_whitespace_n(s, max)\n            }\n        }\n    };\n\n    // Convert to list of strings (using interned empty string when applicable)\n    let mut list_items = Vec::with_capacity(parts.len());\n    for part in parts {\n        vm.heap.check_time()?;\n        list_items.push(allocate_string(part.to_owned(), vm.heap)?);\n    }\n\n    let list = crate::types::List::new(list_items);\n    let heap_id = vm.heap.allocate(HeapData::List(list))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Parses arguments for split methods.\n///\n/// Supports both positional and keyword arguments for sep and maxsplit.\nfn parse_split_args(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(Option<String>, i64)> {\n    let (pos, kwargs) = args.into_parts();\n    let kwargs_iter = kwargs.into_iter();\n    defer_drop_mut!(kwargs_iter, vm);\n\n    let mut pos_iter = pos;\n    let sep_value = pos_iter.next();\n    defer_drop_mut!(sep_value, vm);\n    let maxsplit_value = pos_iter.next();\n    defer_drop_mut!(maxsplit_value, vm);\n\n    // Check no extra positional arguments\n    if pos_iter.len() != 0 {\n        return Err(ExcType::type_error_at_most(method, 2, 3));\n    }\n\n    // Extract positional sep (default None)\n    let mut has_pos_sep = sep_value.is_some();\n    let mut sep = if let Some(v) = sep_value.as_ref() {\n        if matches!(v, Value::None) {\n            None\n        } else {\n            Some(extract_string_arg(v, vm)?)\n        }\n    } else {\n        None\n    };\n\n    // Extract positional maxsplit (default -1)\n    let mut has_pos_maxsplit = maxsplit_value.is_some();\n    let mut maxsplit = if let Some(v) = maxsplit_value.as_ref() {\n        extract_int_arg(v, vm)?\n    } else {\n        -1\n    };\n\n    // Process kwargs\n    for (key, value) in kwargs_iter {\n        defer_drop!(key, vm);\n        defer_drop!(value, vm);\n\n        let Some(keyword_name) = key.as_either_str(vm.heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(vm.interns);\n        match key_str {\n            \"sep\" => {\n                if has_pos_sep {\n                    return Err(ExcType::type_error(format!(\n                        \"{method}() got multiple values for argument 'sep'\"\n                    )));\n                }\n                if matches!(value, Value::None) {\n                    sep = None;\n                } else {\n                    sep = Some(extract_string_arg(value, vm)?);\n                }\n                has_pos_sep = true;\n            }\n            \"maxsplit\" => {\n                if has_pos_maxsplit {\n                    return Err(ExcType::type_error(format!(\n                        \"{method}() got multiple values for argument 'maxsplit'\"\n                    )));\n                }\n                maxsplit = extract_int_arg(value, vm)?;\n                has_pos_maxsplit = true;\n            }\n            _ => {\n                return Err(ExcType::type_error(format!(\n                    \"'{key_str}' is an invalid keyword argument for {method}()\"\n                )));\n            }\n        }\n    }\n\n    Ok((sep, maxsplit))\n}\n\n/// Split string on whitespace, returning at most `maxsplit + 1` parts.\nfn split_whitespace_n(s: &str, maxsplit: usize) -> Vec<&str> {\n    let mut parts = Vec::new();\n    let mut remaining = s.trim_start();\n    let mut count = 0;\n\n    while !remaining.is_empty() && count < maxsplit {\n        if let Some(end) = remaining.find(|c: char| c.is_whitespace()) {\n            parts.push(&remaining[..end]);\n            remaining = remaining[end..].trim_start();\n            count += 1;\n        } else {\n            break;\n        }\n    }\n\n    if !remaining.is_empty() {\n        parts.push(remaining);\n    }\n\n    parts\n}\n\n/// Split string on whitespace from the right, returning at most `maxsplit + 1` parts.\nfn rsplit_whitespace_n(s: &str, maxsplit: usize) -> Vec<&str> {\n    let mut parts = Vec::new();\n    let mut remaining = s.trim_end();\n    let mut count = 0;\n\n    while !remaining.is_empty() && count < maxsplit {\n        if let Some(start) = remaining.rfind(|c: char| c.is_whitespace()) {\n            parts.push(&remaining[start + 1..]);\n            remaining = remaining[..start].trim_end();\n            count += 1;\n        } else {\n            break;\n        }\n    }\n\n    if !remaining.is_empty() {\n        parts.push(remaining);\n    }\n\n    parts.reverse();\n    parts\n}\n\n/// Implements Python's `str.splitlines(keepends?)` method.\n///\n/// Returns a list of the lines in the string, breaking at line boundaries.\n/// Accepts keepends as either positional or keyword argument.\nfn str_splitlines(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let keepends = parse_splitlines_args(args, vm)?;\n\n    let mut lines = Vec::new();\n    let mut start = 0;\n    let bytes = s.as_bytes();\n    let len = bytes.len();\n\n    while start < len {\n        vm.heap.check_time()?;\n\n        // Find the next line ending\n        let mut end = start;\n        let mut line_end = start;\n\n        while end < len {\n            match bytes[end] {\n                b'\\n' => {\n                    line_end = end;\n                    end += 1;\n                    break;\n                }\n                b'\\r' => {\n                    line_end = end;\n                    end += 1;\n                    // Check for \\r\\n\n                    if end < len && bytes[end] == b'\\n' {\n                        end += 1;\n                    }\n                    break;\n                }\n                _ => {\n                    end += 1;\n                    line_end = end;\n                }\n            }\n        }\n\n        let line = if keepends { &s[start..end] } else { &s[start..line_end] };\n\n        lines.push(allocate_string(line.to_owned(), vm.heap)?);\n\n        start = end;\n    }\n\n    let list = crate::types::List::new(lines);\n    let heap_id = vm.heap.allocate(HeapData::List(list))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Parses arguments for splitlines method.\n///\n/// Supports both positional and keyword arguments for keepends.\nfn parse_splitlines_args(args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n    let (pos, kwargs) = args.into_parts();\n    let kwargs_iter = kwargs.into_iter();\n    defer_drop_mut!(kwargs_iter, vm);\n\n    let mut pos_iter = pos;\n    let keepends_value = pos_iter.next();\n    defer_drop_mut!(keepends_value, vm);\n\n    // Check no extra positional arguments\n    if pos_iter.len() != 0 {\n        return Err(ExcType::type_error_at_most(\"str.splitlines\", 1, 2));\n    }\n\n    // Extract positional keepends (default false)\n    let mut has_pos_keepends = keepends_value.is_some();\n    let mut keepends = if let Some(v) = keepends_value.as_ref() {\n        value_is_truthy(v)\n    } else {\n        false\n    };\n\n    // Process kwargs\n    for (key, value) in kwargs_iter {\n        defer_drop!(key, vm);\n        defer_drop!(value, vm);\n\n        let Some(keyword_name) = key.as_either_str(vm.heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(vm.interns);\n        if key_str == \"keepends\" {\n            if has_pos_keepends {\n                return Err(ExcType::type_error(\n                    \"str.splitlines() got multiple values for argument 'keepends'\",\n                ));\n            }\n            keepends = value_is_truthy(value);\n            has_pos_keepends = true;\n        } else {\n            return Err(ExcType::type_error(format!(\n                \"'{key_str}' is an invalid keyword argument for str.splitlines()\"\n            )));\n        }\n    }\n\n    Ok(keepends)\n}\n\n/// Checks if a value is truthy for bool conversion.\nfn value_is_truthy(v: &Value) -> bool {\n    match v {\n        Value::Bool(b) => *b,\n        Value::Int(i) => *i != 0,\n        Value::None => false,\n        _ => true, // Most other values are truthy\n    }\n}\n\n/// Implements Python's `str.partition(sep)` method.\n///\n/// Splits the string at the first occurrence of sep, and returns a 3-tuple\n/// containing the part before the separator, the separator itself, and the part after.\nfn str_partition(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let sep_value = args.get_one_arg(\"str.partition\", vm.heap)?;\n    defer_drop!(sep_value, vm);\n    let sep = extract_string_arg(sep_value, vm)?;\n\n    if sep.is_empty() {\n        return Err(ExcType::value_error_empty_separator());\n    }\n\n    let (before, sep_found, after) = match s.find(&sep) {\n        Some(pos) => (&s[..pos], &sep[..], &s[pos + sep.len()..]),\n        None => (s, \"\", \"\"),\n    };\n\n    let before_val = allocate_string(before.to_owned(), vm.heap)?;\n    let sep_val = allocate_string(sep_found.to_owned(), vm.heap)?;\n    let after_val = allocate_string(after.to_owned(), vm.heap)?;\n\n    Ok(crate::types::allocate_tuple(\n        smallvec![before_val, sep_val, after_val],\n        vm.heap,\n    )?)\n}\n\n/// Implements Python's `str.rpartition(sep)` method.\n///\n/// Splits the string at the last occurrence of sep, and returns a 3-tuple\n/// containing the part before the separator, the separator itself, and the part after.\nfn str_rpartition(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let sep_value = args.get_one_arg(\"str.rpartition\", vm.heap)?;\n    defer_drop!(sep_value, vm);\n    let sep = extract_string_arg(sep_value, vm)?;\n\n    if sep.is_empty() {\n        return Err(ExcType::value_error_empty_separator());\n    }\n\n    let (before, sep_found, after) = match s.rfind(&sep) {\n        Some(pos) => (&s[..pos], &sep[..], &s[pos + sep.len()..]),\n        None => (\"\", \"\", s),\n    };\n\n    let before_val = allocate_string(before.to_owned(), vm.heap)?;\n    let sep_val = allocate_string(sep_found.to_owned(), vm.heap)?;\n    let after_val = allocate_string(after.to_owned(), vm.heap)?;\n\n    Ok(crate::types::allocate_tuple(\n        smallvec![before_val, sep_val, after_val],\n        vm.heap,\n    )?)\n}\n\n// =============================================================================\n// Replace/modify methods\n// =============================================================================\n\n/// Implements Python's `str.replace(old, new, count?)` method.\n///\n/// Returns a copy with all occurrences of substring old replaced by new.\n/// If count is given, only the first count occurrences are replaced.\nfn str_replace(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (old, new, count) = parse_replace_args(\"str.replace\", args, vm)?;\n\n    check_replace_size(s.len(), old.len(), new.len(), count, vm.heap.tracker())?;\n\n    let result = if count < 0 {\n        s.replace(&old, &new)\n    } else {\n        // Safe cast: we've checked count >= 0\n        let n = usize::try_from(count).unwrap_or(usize::MAX);\n        s.replacen(&old, &new, n)\n    };\n\n    allocate_string(result, vm.heap)\n}\n\n/// Parses arguments for the replace method.\n///\n/// Supports both positional and keyword arguments for count (Python 3.13+).\nfn parse_replace_args(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(String, String, i64)> {\n    let (pos, kwargs) = args.into_parts();\n    let kwargs_iter = kwargs.into_iter();\n    defer_drop_mut!(kwargs_iter, vm);\n\n    let mut pos_iter = pos;\n    let Some(old_value) = pos_iter.next() else {\n        return Err(ExcType::type_error_at_least(method, 2, 0));\n    };\n    defer_drop!(old_value, vm);\n\n    let Some(new_value) = pos_iter.next() else {\n        return Err(ExcType::type_error_at_least(method, 2, 1));\n    };\n    defer_drop!(new_value, vm);\n\n    let count_value = pos_iter.next();\n    defer_drop_mut!(count_value, vm);\n\n    // Check no extra positional arguments\n    if pos_iter.len() != 0 {\n        return Err(ExcType::type_error_at_most(method, 3, 4));\n    }\n\n    let old = extract_string_arg(old_value, vm)?;\n    let new = extract_string_arg(new_value, vm)?;\n\n    let mut has_pos_count = count_value.is_some();\n    let mut count = if let Some(v) = count_value.as_ref() {\n        extract_int_arg(v, vm)?\n    } else {\n        -1\n    };\n\n    // Process kwargs (Python 3.13+ allows count as keyword)\n    for (key, value) in kwargs_iter {\n        defer_drop!(key, vm);\n        defer_drop!(value, vm);\n\n        let Some(keyword_name) = key.as_either_str(vm.heap) else {\n            return Err(ExcType::type_error(\"keywords must be strings\"));\n        };\n\n        let key_str = keyword_name.as_str(vm.interns);\n        if key_str == \"count\" {\n            if has_pos_count {\n                return Err(ExcType::type_error(format!(\n                    \"{method}() got multiple values for argument 'count'\"\n                )));\n            }\n            count = extract_int_arg(value, vm)?;\n            has_pos_count = true;\n        } else {\n            return Err(ExcType::type_error(format!(\n                \"'{key_str}' is an invalid keyword argument for {method}()\"\n            )));\n        }\n    }\n\n    Ok((old, new, count))\n}\n\n/// Implements Python's `str.center(width, fillchar?)` method.\n///\n/// Returns centered in a string of length width. Padding is done using the\n/// specified fill character (default is a space).\nfn str_center(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (width, fillchar) = parse_justify_args(\"str.center\", args, vm)?;\n    let len = s.chars().count();\n\n    let result = if width <= len {\n        s.to_owned()\n    } else {\n        check_repeat_size(width, fillchar.len_utf8(), vm.heap.tracker())?;\n        let total_pad = width - len;\n        let left_pad = total_pad / 2;\n        let right_pad = total_pad - left_pad;\n        let mut result = String::with_capacity(width);\n        for _ in 0..left_pad {\n            result.push(fillchar);\n        }\n        result.push_str(s);\n        for _ in 0..right_pad {\n            result.push(fillchar);\n        }\n        result\n    };\n\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.ljust(width, fillchar?)` method.\n///\n/// Returns left-justified in a string of length width.\nfn str_ljust(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (width, fillchar) = parse_justify_args(\"str.ljust\", args, vm)?;\n    let len = s.chars().count();\n\n    let result = if width <= len {\n        s.to_owned()\n    } else {\n        check_repeat_size(width, fillchar.len_utf8(), vm.heap.tracker())?;\n        let pad = width - len;\n        let mut result = String::with_capacity(width);\n        result.push_str(s);\n        for _ in 0..pad {\n            result.push(fillchar);\n        }\n        result\n    };\n\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.rjust(width, fillchar?)` method.\n///\n/// Returns right-justified in a string of length width.\nfn str_rjust(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (width, fillchar) = parse_justify_args(\"str.rjust\", args, vm)?;\n    let len = s.chars().count();\n\n    let result = if width <= len {\n        s.to_owned()\n    } else {\n        check_repeat_size(width, fillchar.len_utf8(), vm.heap.tracker())?;\n        let pad = width - len;\n        let mut result = String::with_capacity(width);\n        for _ in 0..pad {\n            result.push(fillchar);\n        }\n        result.push_str(s);\n        result\n    };\n\n    allocate_string(result, vm.heap)\n}\n\n/// Parses arguments for justify methods (center, ljust, rjust).\nfn parse_justify_args(\n    method: &str,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> RunResult<(usize, char)> {\n    let pos = args.into_pos_only(method, vm.heap)?;\n    defer_drop!(pos, vm);\n\n    match pos.as_slice() {\n        [width_value] => {\n            let w = extract_int_arg(width_value, vm)?;\n            let width = if w < 0 {\n                0\n            } else {\n                usize::try_from(w).unwrap_or(usize::MAX)\n            };\n            Ok((width, ' '))\n        }\n        [width_value, fillchar_value] => {\n            let w = extract_int_arg(width_value, vm)?;\n            let width = if w < 0 {\n                0\n            } else {\n                usize::try_from(w).unwrap_or(usize::MAX)\n            };\n            let fill_str = extract_string_arg(fillchar_value, vm)?;\n            if fill_str.chars().count() != 1 {\n                return Err(ExcType::type_error_fillchar_must_be_single_char());\n            }\n            Ok((width, fill_str.chars().next().unwrap()))\n        }\n        [] => Err(ExcType::type_error_at_least(method, 1, 0)),\n        _ => Err(ExcType::type_error_at_most(method, 2, pos.len())),\n    }\n}\n\n/// Implements Python's `str.zfill(width)` method.\n///\n/// Returns a copy of the string left filled with ASCII '0' digits to make a\n/// string of length width. A sign prefix is handled correctly.\nfn str_zfill(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let width_value = args.get_one_arg(\"str.zfill\", vm.heap)?;\n    defer_drop!(width_value, vm);\n    let width_i64 = extract_int_arg(width_value, vm)?;\n\n    // Safe cast: treat negative as 0, saturate large positive values\n    let width = if width_i64 < 0 {\n        0\n    } else {\n        usize::try_from(width_i64).unwrap_or(usize::MAX)\n    };\n    let len = s.chars().count();\n\n    let result = if width <= len {\n        s.to_owned()\n    } else {\n        // zfill always pads with ASCII '0' (1 byte)\n        check_repeat_size(width, 1, vm.heap.tracker())?;\n        let pad = width - len;\n        let mut chars = s.chars();\n        let first = chars.next();\n\n        let mut result = String::with_capacity(width);\n\n        // Handle sign prefix\n        if matches!(first, Some('+' | '-')) {\n            result.push(first.unwrap());\n            for _ in 0..pad {\n                result.push('0');\n            }\n            result.extend(chars);\n        } else {\n            for _ in 0..pad {\n                result.push('0');\n            }\n            result.push_str(s);\n        }\n        result\n    };\n\n    allocate_string(result, vm.heap)\n}\n\n/// Implements Python's `str.encode(encoding='utf-8', errors='strict')` method.\n///\n/// Returns an encoded version of the string as a bytes object. Only supports\n/// UTF-8 encoding (the native encoding for Rust strings).\nfn str_encode(s: &str, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let (encoding, errors) = parse_encode_args(args, vm)?;\n\n    // Only UTF-8 is supported - Rust strings are always valid UTF-8\n    let encoding_lower = encoding.to_ascii_lowercase();\n    if encoding_lower != \"utf-8\" && encoding_lower != \"utf8\" {\n        return Err(ExcType::lookup_error_unknown_encoding(&encoding));\n    }\n\n    // For UTF-8 encoding of a valid UTF-8 string, errors mode doesn't matter\n    // since there's nothing to handle - the string is already valid UTF-8\n    if errors != \"strict\" && errors != \"ignore\" && errors != \"replace\" && errors != \"backslashreplace\" {\n        return Err(ExcType::lookup_error_unknown_error_handler(&errors));\n    }\n\n    let bytes = s.as_bytes().to_vec();\n    let heap_id = vm.heap.allocate(HeapData::Bytes(Bytes::new(bytes)))?;\n    Ok(Value::Ref(heap_id))\n}\n\n/// Parses arguments for `str.encode()`.\n///\n/// Returns (encoding, errors) with defaults \"utf-8\" and \"strict\".\nfn parse_encode_args(args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<(String, String)> {\n    let (first, second) = args.get_zero_one_two_args(\"str.encode\", vm.heap)?;\n\n    let encoding = if let Some(v) = first {\n        defer_drop!(v, vm);\n        extract_string_arg(v, vm)?\n    } else {\n        \"utf-8\".to_owned()\n    };\n\n    let errors = if let Some(v) = second {\n        defer_drop!(v, vm);\n        extract_string_arg(v, vm)?\n    } else {\n        \"strict\".to_owned()\n    };\n\n    Ok((encoding, errors))\n}\n\n/// Implements Python's `str.isidentifier()` predicate.\n///\n/// Returns True if the string is a valid Python identifier according to\n/// the language definition (starts with letter or underscore, followed by\n/// letters, digits, or underscores). Empty strings return False.\nfn str_isidentifier(s: &str) -> bool {\n    if s.is_empty() {\n        return false;\n    }\n\n    let mut chars = s.chars();\n\n    // First character must be a letter (Unicode) or underscore\n    let first = chars.next().unwrap();\n    if !is_xid_start(first) && first != '_' {\n        return false;\n    }\n\n    // Remaining characters must be letters, digits (Unicode), or underscores\n    chars.all(is_xid_continue)\n}\n\n/// Checks if a character is valid at the start of an identifier (XID_Start).\n///\n/// This is a simplified implementation that covers ASCII and common Unicode letters.\n/// Python uses the full Unicode XID_Start property.\nfn is_xid_start(c: char) -> bool {\n    c.is_alphabetic()\n}\n\n/// Checks if a character is valid in the continuation of an identifier (XID_Continue).\n///\n/// This is a simplified implementation that covers ASCII and common Unicode.\n/// Python uses the full Unicode XID_Continue property.\nfn is_xid_continue(c: char) -> bool {\n    c.is_alphanumeric() || c == '_'\n}\n\n/// Implements Python's `str.istitle()` predicate.\n///\n/// Returns True if the string is titlecased: uppercase characters follow\n/// uncased characters and lowercase characters follow cased characters.\n/// Empty strings return False.\nfn str_istitle(s: &str) -> bool {\n    if s.is_empty() {\n        return false;\n    }\n\n    let mut prev_cased = false;\n    let mut has_cased = false;\n\n    for c in s.chars() {\n        if c.is_uppercase() {\n            // Uppercase must follow uncased\n            if prev_cased {\n                return false;\n            }\n            prev_cased = true;\n            has_cased = true;\n        } else if c.is_lowercase() {\n            // Lowercase must follow cased\n            if !prev_cased {\n                return false;\n            }\n            prev_cased = true;\n            has_cased = true;\n        } else {\n            // Uncased character\n            prev_cased = false;\n        }\n    }\n\n    has_cased\n}\n"
  },
  {
    "path": "crates/monty/src/types/tuple.rs",
    "content": "/// Python tuple type using `SmallVec` for inline storage of small tuples.\n///\n/// This type provides Python tuple semantics. Tuples are immutable sequences\n/// that can contain any Python object. Like lists, tuples properly handle\n/// reference counting for heap-allocated values.\n///\n/// # Optimization\n/// Uses `SmallVec<[Value; 2]>` to store up to 2 elements inline without heap\n/// allocation. This benefits common cases like 2-tuples from `enumerate()`,\n/// `dict.items()`, and function return values.\n///\n/// # Implemented Methods\n/// - `index(value[, start[, end]])` - Find first index of value\n/// - `count(value)` - Count occurrences\n///\n/// All tuple methods from Python's builtins are implemented.\nuse std::cmp::Ordering;\nuse std::fmt::Write;\n\nuse ahash::AHashSet;\nuse smallvec::SmallVec;\n\n/// Inline capacity for small tuples. Tuples with 2 or fewer elements avoid\n/// heap allocation for the items storage.\nconst TUPLE_INLINE_CAPACITY: usize = 3;\n\n/// Storage type for tuple items. Uses SmallVec to inline small tuples.\npub(crate) type TupleVec = SmallVec<[Value; TUPLE_INLINE_CAPACITY]>;\n\nuse super::{\n    MontyIter, PyTrait,\n    list::{get_slice_items, repr_sequence_fmt},\n};\nuse crate::{\n    args::ArgValues,\n    bytecode::{CallResult, VM},\n    defer_drop,\n    exception_private::{ExcType, RunResult},\n    heap::{DropWithHeap, Heap, HeapData, HeapId, HeapItem},\n    intern::StaticStrings,\n    resource::{ResourceError, ResourceTracker},\n    types::Type,\n    value::{EitherStr, Value},\n};\n\n/// Python tuple value stored on the heap.\n///\n/// Uses `SmallVec<[Value; 3]>` internally to avoid separate heap allocation\n/// for tuples with 3 or fewer elements. This is a significant optimization\n/// since small tuples are very common (enumerate, dict items, returns, etc.).\n///\n/// # Reference Counting\n/// When a tuple is freed, all contained heap references have their refcounts\n/// decremented via `push_stack_ids`.\n///\n/// # GC Optimization\n/// The `contains_refs` flag tracks whether the tuple contains any `Value::Ref` items.\n/// This allows `collect_child_ids` and `py_dec_ref_ids` to skip iteration when the\n/// tuple contains only primitive values (ints, bools, None, etc.).\n#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]\npub(crate) struct Tuple {\n    items: TupleVec,\n    /// True if any item in the tuple is a `Value::Ref`. Set at creation time\n    /// since tuples are immutable.\n    contains_refs: bool,\n}\n\nimpl Tuple {\n    /// Creates a new tuple from a vector of values.\n    ///\n    /// Automatically computes the `contains_refs` flag by checking if any value\n    /// is a `Value::Ref`. Since tuples are immutable, this flag never changes.\n    ///\n    /// For tuples with 3 or fewer elements, the items are stored inline in the\n    /// SmallVec without additional heap allocation.\n    ///\n    /// Note: This does NOT increment reference counts - the caller must\n    /// ensure refcounts are properly managed.\n    #[must_use]\n    fn new(items: TupleVec) -> Self {\n        let contains_refs = items.iter().any(|v| matches!(v, Value::Ref(_)));\n        Self { items, contains_refs }\n    }\n\n    /// Returns a reference to the underlying SmallVec.\n    #[must_use]\n    pub fn as_slice(&self) -> &[Value] {\n        &self.items\n    }\n\n    /// Returns whether the tuple contains any heap references.\n    ///\n    /// When false, `collect_child_ids` and `py_dec_ref_ids` can skip iteration.\n    #[inline]\n    #[must_use]\n    pub fn contains_refs(&self) -> bool {\n        self.contains_refs\n    }\n\n    /// Creates a tuple from the `tuple()` constructor call.\n    ///\n    /// - `tuple()` with no args returns an empty tuple (singleton)\n    /// - `tuple(iterable)` creates a tuple from any iterable (list, tuple, range, str, bytes, dict)\n    pub fn init(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        let value = args.get_zero_one_arg(\"tuple\", vm.heap)?;\n        match value {\n            None => {\n                // Use empty tuple singleton\n                Ok(vm.heap.get_empty_tuple())\n            }\n            Some(v) => {\n                let items = MontyIter::new(v, vm)?.collect(vm)?;\n                Ok(allocate_tuple(items, vm.heap)?)\n            }\n        }\n    }\n}\n\nimpl From<Tuple> for Vec<Value> {\n    fn from(tuple: Tuple) -> Self {\n        tuple.items.into_vec()\n    }\n}\n\nimpl From<Tuple> for TupleVec {\n    fn from(tuple: Tuple) -> Self {\n        tuple.items\n    }\n}\n\n/// Allocates a tuple, using the empty tuple singleton when appropriate.\n///\n/// This is the preferred way to allocate tuples as it provides:\n/// - Empty tuple interning: `() is ()` returns `True`\n/// - SmallVec optimization for small tuples (≤3 elements)\n///\n/// # Example Usage\n/// ```ignore\n/// // Empty tuple - returns singleton\n/// let empty = allocate_tuple(Vec::new(), heap)?;\n///\n/// // Small tuple - stored inline in SmallVec\n/// let pair = allocate_tuple(vec![Value::Int(1), Value::Int(2)], heap)?;\n/// ```\npub fn allocate_tuple(\n    items: SmallVec<[Value; TUPLE_INLINE_CAPACITY]>,\n    heap: &mut Heap<impl ResourceTracker>,\n) -> Result<Value, crate::resource::ResourceError> {\n    if items.is_empty() {\n        Ok(heap.get_empty_tuple())\n    } else {\n        // Allocate a new tuple (SmallVec will inline if ≤3 elements)\n        let heap_id = heap.allocate(HeapData::Tuple(Tuple::new(items)))?;\n        Ok(Value::Ref(heap_id))\n    }\n}\n\nimpl PyTrait for Tuple {\n    fn py_type(&self, _heap: &Heap<impl ResourceTracker>) -> Type {\n        Type::Tuple\n    }\n\n    fn py_len(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        Some(self.items.len())\n    }\n\n    fn py_getitem(&self, key: &Value, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n        let heap = &mut *vm.heap;\n        // Check for slice first (Value::Ref pointing to HeapData::Slice)\n        if let Value::Ref(id) = key\n            && let HeapData::Slice(slice) = heap.get(*id)\n        {\n            let (start, stop, step) = slice\n                .indices(self.items.len())\n                .map_err(|()| ExcType::value_error_slice_step_zero())?;\n\n            let items = get_slice_items(&self.items, start, stop, step, heap)?;\n            return Ok(allocate_tuple(items.into(), heap)?);\n        }\n\n        // Extract integer index, accepting Int, Bool (True=1, False=0), and LongInt\n        let index = key.as_index(heap, Type::Tuple)?;\n\n        // Convert to usize, handling negative indices (Python-style: -1 = last element)\n        let len = i64::try_from(self.items.len()).expect(\"tuple length exceeds i64::MAX\");\n        let normalized_index = if index < 0 { index + len } else { index };\n\n        // Bounds check\n        if normalized_index < 0 || normalized_index >= len {\n            return Err(ExcType::tuple_index_error());\n        }\n\n        // Return clone of the item with proper refcount increment\n        // Safety: normalized_index is validated to be in [0, len) above\n        let idx = usize::try_from(normalized_index).expect(\"tuple index validated non-negative\");\n        Ok(self.items[idx].clone_with_heap(heap))\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        if self.items.len() != other.items.len() {\n            return Ok(false);\n        }\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        for (i1, i2) in self.items.iter().zip(&other.items) {\n            vm.heap.check_time()?;\n            if !i1.py_eq(i2, vm)? {\n                return Ok(false);\n            }\n        }\n        Ok(true)\n    }\n\n    /// Lexicographic comparison for tuples.\n    ///\n    /// Compares element-by-element left-to-right. The first non-equal pair\n    /// determines the result. If all compared elements are equal, the shorter\n    /// tuple is considered less than the longer one — matching Python semantics:\n    /// `(1, 2) < (1, 2, 3)` is `True`.\n    ///\n    /// Returns `None` if any element pair is incomparable (e.g. `int` vs `str`).\n    fn py_cmp(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Ordering>, ResourceError> {\n        let token = vm.heap.incr_recursion_depth()?;\n        defer_drop!(token, vm);\n        for (av, bv) in self.items.iter().zip(&other.items) {\n            vm.heap.check_time()?;\n            match av.py_cmp(bv, vm)? {\n                Some(Ordering::Equal) => {}\n                Some(ord) => return Ok(Some(ord)),\n                None => {\n                    // py_cmp returned None — the elements don't support ordering.\n                    // CPython checks __eq__ first and only calls __lt__ for non-equal\n                    // pairs, so equal-but-unorderable elements (e.g. None == None)\n                    // should be treated as equal and not block comparison.\n                    if !av.py_eq(bv, vm)? {\n                        return Ok(None);\n                    }\n                }\n            }\n        }\n        // All compared elements equal — shorter tuple is less\n        Ok(Some(self.items.len().cmp(&other.items.len())))\n    }\n\n    fn py_add(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, crate::resource::ResourceError> {\n        let heap = &mut *vm.heap;\n        // Clone both tuples' contents with proper refcounting\n        let mut result: TupleVec = self.items.iter().map(|obj| obj.clone_with_heap(heap)).collect();\n        let other_cloned = other.items.iter().map(|obj| obj.clone_with_heap(heap));\n        result.extend(other_cloned);\n        Ok(Some(allocate_tuple(result, heap)?))\n    }\n\n    fn py_call_attr(\n        &mut self,\n        _self_id: HeapId,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        attr: &EitherStr,\n        args: ArgValues,\n    ) -> RunResult<CallResult> {\n        match attr.static_string() {\n            Some(StaticStrings::Index) => tuple_index(self, args, vm).map(CallResult::Value),\n            Some(StaticStrings::Count) => tuple_count(self, args, vm).map(CallResult::Value),\n            _ => {\n                args.drop_with_heap(vm);\n                Err(ExcType::attribute_error(Type::Tuple, attr.as_str(vm.interns)))\n            }\n        }\n    }\n\n    fn py_bool(&self, _vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        !self.items.is_empty()\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        repr_sequence_fmt('(', ')', &self.items, f, vm, heap_ids)\n    }\n}\n\nimpl HeapItem for Tuple {\n    fn py_estimate_size(&self) -> usize {\n        std::mem::size_of::<Self>() + self.items.len() * std::mem::size_of::<Value>()\n    }\n\n    /// Pushes all heap IDs contained in this tuple onto the stack.\n    ///\n    /// Called during garbage collection to decrement refcounts of nested values.\n    /// When `ref-count-panic` is enabled, also marks all Values as Dereferenced.\n    fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        // Skip iteration if no refs - GC optimization for tuples of primitives\n        if !self.contains_refs {\n            return;\n        }\n        for obj in &mut self.items {\n            if let Value::Ref(id) = obj {\n                stack.push(*id);\n                #[cfg(feature = \"ref-count-panic\")]\n                obj.dec_ref_forget();\n            }\n        }\n    }\n}\n\n/// Implements Python's `tuple.index(value[, start[, end]])` method.\n///\n/// Returns the index of the first occurrence of value.\n/// Raises ValueError if the value is not found.\nfn tuple_index(tuple: &Tuple, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let pos_args = args.into_pos_only(\"tuple.index\", vm.heap)?;\n    defer_drop!(pos_args, vm);\n\n    let len = tuple.as_slice().len();\n    let (value, start, end) = match pos_args.as_slice() {\n        [] => return Err(ExcType::type_error_at_least(\"tuple.index\", 1, 0)),\n        [value] => (value, 0, len),\n        [value, start_arg] => {\n            let start = normalize_tuple_index(start_arg.as_int(vm.heap)?, len);\n            (value, start, len)\n        }\n        [value, start_arg, end_arg] => {\n            let start = normalize_tuple_index(start_arg.as_int(vm.heap)?, len);\n            let end = normalize_tuple_index(end_arg.as_int(vm.heap)?, len).max(start);\n            (value, start, end)\n        }\n        other => return Err(ExcType::type_error_at_most(\"tuple.index\", 3, other.len())),\n    };\n\n    // Search for the value in the specified range\n    for (i, item) in tuple.as_slice()[start..end].iter().enumerate() {\n        if value.py_eq(item, vm)? {\n            let idx = i64::try_from(start + i).expect(\"index exceeds i64::MAX\");\n            return Ok(Value::Int(idx));\n        }\n    }\n\n    Err(ExcType::value_error_not_in_tuple())\n}\n\n/// Implements Python's `tuple.count(value)` method.\n///\n/// Returns the number of occurrences of value in the tuple.\nfn tuple_count(tuple: &Tuple, args: ArgValues, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Value> {\n    let value = args.get_one_arg(\"tuple.count\", vm.heap)?;\n    defer_drop!(value, vm);\n\n    let mut count = 0usize;\n    for item in tuple.as_slice() {\n        if value.py_eq(item, vm)? {\n            count += 1;\n        }\n    }\n\n    let count_i64 = i64::try_from(count).expect(\"count exceeds i64::MAX\");\n    Ok(Value::Int(count_i64))\n}\n\n/// Normalizes a Python-style tuple index to a valid index in range [0, len].\nfn normalize_tuple_index(index: i64, len: usize) -> usize {\n    if index < 0 {\n        let abs_index = usize::try_from(-index).unwrap_or(usize::MAX);\n        len.saturating_sub(abs_index)\n    } else {\n        usize::try_from(index).unwrap_or(len).min(len)\n    }\n}\n"
  },
  {
    "path": "crates/monty/src/types/type.rs",
    "content": "use std::fmt;\n\nuse num_bigint::BigInt;\n\nuse crate::{\n    args::ArgValues,\n    bytecode::VM,\n    defer_drop,\n    exception_private::{ExcType, RunError, RunResult, SimpleException},\n    heap::{DropWithHeap, Heap, HeapData},\n    intern::{StaticStrings, StringId},\n    resource::ResourceTracker,\n    types::{\n        Bytes, Dict, FrozenSet, List, LongInt, MontyIter, Path, PyTrait, Range, Set, Slice, Str, Tuple,\n        bytes::bytes_fromhex, dict::dict_fromkeys, str::StringRepr,\n    },\n    value::Value,\n};\n\n/// Represents the Python type of a value.\n///\n/// This enum is used both for type checking and as a callable constructor.\n/// Some variants are Python builtins accessible by name (e.g., `int`, `list`),\n/// while others are internal types only available through imports or introspection\n/// (e.g., `TextIOWrapper`, `PosixPath`).\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\n#[expect(clippy::enum_variant_names)]\npub enum Type {\n    Ellipsis,\n    Type,\n    NoneType,\n    Bool,\n    Int,\n    Float,\n    Range,\n    Slice,\n    Str,\n    Bytes,\n    List,\n    Tuple,\n    NamedTuple,\n    Dict,\n    DictKeys,\n    DictItems,\n    DictValues,\n    Set,\n    FrozenSet,\n    Dataclass,\n    Exception(ExcType),\n    Function,\n    BuiltinFunction,\n    Cell,\n    Iterator,\n    /// Coroutine type for async functions and external futures.\n    Coroutine,\n    Module,\n    /// Marker types like stdout/stderr - displays as \"TextIOWrapper\"\n    TextIOWrapper,\n    /// typing module special forms (Any, Optional, Union, etc.) - displays as \"typing._SpecialForm\"\n    SpecialForm,\n    /// A filesystem path from `pathlib.Path` - displays as \"PosixPath\"\n    Path,\n    /// A property descriptor - displays as \"property\"\n    Property,\n    /// A compiled regex pattern from `re.compile()` - displays as \"re.Pattern\"\n    RePattern,\n    /// A regex match result from `re.match()` / `re.search()` etc. - displays as \"re.Match\"\n    ReMatch,\n}\n\nimpl fmt::Display for Type {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Ellipsis => f.write_str(\"ellipsis\"),\n            Self::Type => f.write_str(\"type\"),\n            Self::NoneType => f.write_str(\"NoneType\"),\n            Self::Bool => f.write_str(\"bool\"),\n            Self::Int => f.write_str(\"int\"),\n            Self::Float => f.write_str(\"float\"),\n            Self::Range => f.write_str(\"range\"),\n            Self::Slice => f.write_str(\"slice\"),\n            Self::Str => f.write_str(\"str\"),\n            Self::Bytes => f.write_str(\"bytes\"),\n            Self::List => f.write_str(\"list\"),\n            Self::Tuple => f.write_str(\"tuple\"),\n            Self::NamedTuple => f.write_str(\"namedtuple\"),\n            Self::Dict => f.write_str(\"dict\"),\n            Self::DictKeys => f.write_str(\"dict_keys\"),\n            Self::DictItems => f.write_str(\"dict_items\"),\n            Self::DictValues => f.write_str(\"dict_values\"),\n            Self::Set => f.write_str(\"set\"),\n            Self::FrozenSet => f.write_str(\"frozenset\"),\n            Self::Dataclass => f.write_str(\"dataclass\"),\n            Self::Exception(exc_type) => write!(f, \"{exc_type}\"),\n            Self::Function => f.write_str(\"function\"),\n            Self::BuiltinFunction => f.write_str(\"builtin_function_or_method\"),\n            Self::Cell => f.write_str(\"cell\"),\n            Self::Iterator => f.write_str(\"iterator\"),\n            Self::Coroutine => f.write_str(\"coroutine\"),\n            Self::Module => f.write_str(\"module\"),\n            Self::TextIOWrapper => f.write_str(\"_io.TextIOWrapper\"),\n            Self::SpecialForm => f.write_str(\"typing._SpecialForm\"),\n            Self::Path => f.write_str(\"PosixPath\"),\n            Self::Property => f.write_str(\"property\"),\n            Self::RePattern => f.write_str(\"re.Pattern\"),\n            Self::ReMatch => f.write_str(\"re.Match\"),\n        }\n    }\n}\n\nimpl Type {\n    /// Returns the Python source-level name for builtin types that can be called directly.\n    ///\n    /// This differs from `Display` for internal representation-only names such as\n    /// `Type::Iterator`, which displays as `iterator` for repr/type output but is\n    /// exposed as the builtin constructor `iter` in Python source.\n    #[must_use]\n    pub const fn builtin_name(self) -> Option<&'static str> {\n        match self {\n            Self::Bool => Some(\"bool\"),\n            Self::Int => Some(\"int\"),\n            Self::Float => Some(\"float\"),\n            Self::Str => Some(\"str\"),\n            Self::Bytes => Some(\"bytes\"),\n            Self::List => Some(\"list\"),\n            Self::Tuple => Some(\"tuple\"),\n            Self::Dict => Some(\"dict\"),\n            Self::Set => Some(\"set\"),\n            Self::FrozenSet => Some(\"frozenset\"),\n            Self::Range => Some(\"range\"),\n            Self::Slice => Some(\"slice\"),\n            Self::Iterator => Some(\"iter\"),\n            Self::Type => Some(\"type\"),\n            Self::Property => Some(\"property\"),\n            _ => None,\n        }\n    }\n\n    /// Resolves a bare Python name to a builtin type, if it is one.\n    ///\n    /// Only matches names that are true Python builtins — accessible without any import.\n    /// Internal types like `TextIOWrapper`, `PosixPath`, `NoneType`, and `ellipsis` are\n    /// intentionally excluded because they require imports or are not directly nameable.\n    ///\n    /// This replaces the previous strum `FromStr` derive which matched ALL variants,\n    /// including internal types that shouldn't be resolvable from bare names.\n    #[must_use]\n    pub fn from_builtin_name(name: &str) -> Option<Self> {\n        match name {\n            \"bool\" => Some(Self::Bool),\n            \"int\" => Some(Self::Int),\n            \"float\" => Some(Self::Float),\n            \"str\" => Some(Self::Str),\n            \"bytes\" => Some(Self::Bytes),\n            \"list\" => Some(Self::List),\n            \"tuple\" => Some(Self::Tuple),\n            \"dict\" => Some(Self::Dict),\n            \"set\" => Some(Self::Set),\n            \"frozenset\" => Some(Self::FrozenSet),\n            \"range\" => Some(Self::Range),\n            \"slice\" => Some(Self::Slice),\n            \"iter\" => Some(Self::Iterator),\n            \"type\" => Some(Self::Type),\n            \"property\" => Some(Self::Property),\n            _ => None,\n        }\n    }\n\n    /// Checks if a value of type `self` is an instance of `other`.\n    ///\n    /// This handles Python's subtype relationships:\n    /// - `bool` is a subtype of `int` (so `isinstance(True, int)` returns True)\n    #[must_use]\n    pub fn is_instance_of(self, other: Self) -> bool {\n        if self == other {\n            true\n        } else if self == Self::Bool && other == Self::Int {\n            // bool is a subtype of int in Python\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Converts a callable type to a u8 for the `CallBuiltinType` opcode.\n    ///\n    /// Returns `Some(u8)` for types that can be called as constructors,\n    /// `None` for non-callable types.\n    #[must_use]\n    pub fn callable_to_u8(self) -> Option<u8> {\n        match self {\n            Self::Bool => Some(0),\n            Self::Int => Some(1),\n            Self::Float => Some(2),\n            Self::Str => Some(3),\n            Self::Bytes => Some(4),\n            Self::List => Some(5),\n            Self::Tuple => Some(6),\n            Self::Dict => Some(7),\n            Self::Set => Some(8),\n            Self::FrozenSet => Some(9),\n            Self::Range => Some(10),\n            Self::Slice => Some(11),\n            Self::Iterator => Some(12),\n            Self::Path => Some(13),\n            _ => None,\n        }\n    }\n\n    /// Converts a u8 back to a callable `Type` for the `CallBuiltinType` opcode.\n    ///\n    /// Returns `Some(Type)` for valid callable type IDs, `None` otherwise.\n    #[must_use]\n    pub fn callable_from_u8(id: u8) -> Option<Self> {\n        match id {\n            0 => Some(Self::Bool),\n            1 => Some(Self::Int),\n            2 => Some(Self::Float),\n            3 => Some(Self::Str),\n            4 => Some(Self::Bytes),\n            5 => Some(Self::List),\n            6 => Some(Self::Tuple),\n            7 => Some(Self::Dict),\n            8 => Some(Self::Set),\n            9 => Some(Self::FrozenSet),\n            10 => Some(Self::Range),\n            11 => Some(Self::Slice),\n            12 => Some(Self::Iterator),\n            13 => Some(Self::Path),\n            _ => None,\n        }\n    }\n\n    /// Calls this type as a constructor (e.g., `list(x)`, `int(x)`).\n    ///\n    /// Dispatches to the appropriate type's init method for container types,\n    /// or handles primitive type conversions inline.\n    pub(crate) fn call(self, vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {\n        match self {\n            // Container types - delegate to init methods\n            Self::List => List::init(vm, args),\n            Self::Tuple => Tuple::init(vm, args),\n            Self::Dict => Dict::init(vm, args),\n            Self::Set => Set::init(vm, args),\n            Self::FrozenSet => FrozenSet::init(vm, args),\n            Self::Str => Str::init(vm, args),\n            Self::Bytes => Bytes::init(vm, args),\n            Self::Range => Range::init(vm, args),\n            Self::Slice => Slice::init(vm, args),\n            Self::Iterator => MontyIter::init(vm, args),\n            Self::Path => Path::init(vm, args),\n\n            // Primitive types - inline implementation\n            Self::Int => {\n                let heap = &mut *vm.heap;\n                let interns = vm.interns;\n                let Some(v) = args.get_zero_one_arg(\"int\", heap)? else {\n                    return Ok(Value::Int(0));\n                };\n                defer_drop!(v, heap);\n                match v {\n                    Value::Int(i) => Ok(Value::Int(*i)),\n                    Value::Float(f) => Ok(Value::Int(f64_to_i64_truncate(*f))),\n                    Value::Bool(b) => Ok(Value::Int(i64::from(*b))),\n                    Value::InternString(string_id) => parse_int_from_str(interns.get_str(*string_id), heap),\n                    Value::Ref(heap_id) => {\n                        // Clone data to release the borrow on heap before mutation\n                        match heap.get(*heap_id) {\n                            HeapData::Str(s) => {\n                                let s = s.to_string();\n                                parse_int_from_str(&s, heap)\n                            }\n                            HeapData::LongInt(li) => li.clone().into_value(heap).map_err(Into::into),\n                            _ => Err(ExcType::type_error_int_conversion(v.py_type(heap))),\n                        }\n                    }\n                    _ => Err(ExcType::type_error_int_conversion(v.py_type(heap))),\n                }\n            }\n            Self::Float => {\n                let heap = &mut *vm.heap;\n                let interns = vm.interns;\n                let Some(v) = args.get_zero_one_arg(\"float\", heap)? else {\n                    return Ok(Value::Float(0.0));\n                };\n                defer_drop!(v, heap);\n                match v {\n                    Value::Float(f) => Ok(Value::Float(*f)),\n                    Value::Int(i) => Ok(Value::Float(*i as f64)),\n                    Value::Bool(b) => Ok(Value::Float(if *b { 1.0 } else { 0.0 })),\n                    Value::InternString(string_id) => {\n                        Ok(Value::Float(parse_f64_from_str(interns.get_str(*string_id))?))\n                    }\n                    Value::Ref(heap_id) => match heap.get(*heap_id) {\n                        HeapData::Str(s) => Ok(Value::Float(parse_f64_from_str(s.as_str())?)),\n                        _ => Err(ExcType::type_error_float_conversion(v.py_type(heap))),\n                    },\n                    _ => Err(ExcType::type_error_float_conversion(v.py_type(heap))),\n                }\n            }\n            Self::Bool => {\n                let Some(v) = args.get_zero_one_arg(\"bool\", vm.heap)? else {\n                    return Ok(Value::Bool(false));\n                };\n                defer_drop!(v, vm);\n                Ok(Value::Bool(v.py_bool(vm)))\n            }\n\n            // Non-callable types - raise TypeError\n            _ => Err(ExcType::type_error_not_callable(self)),\n        }\n    }\n}\n\n/// Truncates f64 to i64 with clamping for out-of-range values.\n///\n/// Python's `int(float)` truncates toward zero. For values outside i64 range,\n/// we clamp to i64::MAX/MIN (Python would use arbitrary precision ints, which\n/// we don't support).\nfn f64_to_i64_truncate(value: f64) -> i64 {\n    // trunc() rounds toward zero, matching Python's int(float) behavior\n    let truncated = value.trunc();\n    if truncated >= i64::MAX as f64 {\n        i64::MAX\n    } else if truncated <= i64::MIN as f64 {\n        i64::MIN\n    } else {\n        // SAFETY for clippy: truncated is guaranteed to be in (i64::MIN, i64::MAX)\n        // after the bounds checks above, so truncation cannot overflow\n        #[expect(clippy::cast_possible_truncation, reason = \"bounds checked above\")]\n        let result = truncated as i64;\n        result\n    }\n}\n\n/// Parses a Python `float()` string argument into an `f64`.\n///\n/// This supports:\n/// - Leading/trailing whitespace (e.g. `\"  1.5  \"`)\n/// - The special values `inf`, `-inf`, `infinity`, and `nan` (case-insensitive)\n///\n/// Underscore digit separators are not currently supported.\nfn parse_f64_from_str(value: &str) -> RunResult<f64> {\n    let trimmed = value.trim();\n    if trimmed.is_empty() {\n        return Err(value_error_could_not_convert_string_to_float(value));\n    }\n\n    let lower = trimmed.to_ascii_lowercase();\n    let parsed = match lower.as_str() {\n        \"inf\" | \"+inf\" | \"infinity\" | \"+infinity\" => f64::INFINITY,\n        \"-inf\" | \"-infinity\" => f64::NEG_INFINITY,\n        \"nan\" | \"+nan\" => f64::NAN,\n        \"-nan\" => -f64::NAN,\n        _ => trimmed\n            .parse::<f64>()\n            .map_err(|_| value_error_could_not_convert_string_to_float(value))?,\n    };\n\n    Ok(parsed)\n}\n\n/// Creates the `ValueError` raised by `float()` when a string cannot be parsed.\n///\n/// Matches CPython's message format: `could not convert string to float: '...'`.\nfn value_error_could_not_convert_string_to_float(value: &str) -> RunError {\n    SimpleException::new_msg(\n        ExcType::ValueError,\n        format!(\"could not convert string to float: {}\", StringRepr(value)),\n    )\n    .into()\n}\n\n/// Parses a Python `int()` string argument into an `Int` or `LongInt`.\n///\n/// Handles whitespace stripping and removing `_` separators. Returns `Value::Int` if the value\n/// fits in i64, otherwise allocates a `LongInt` on the heap. Returns `ValueError` on failure.\nfn parse_int_from_str(value: &str, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {\n    // Try parsing as i64 first (fast path)\n    if let Ok(int) = value.parse::<i64>() {\n        return Ok(Value::Int(int));\n    }\n    let trimmed = value.trim();\n\n    if let Ok(int) = trimmed.parse::<i64>() {\n        return Ok(Value::Int(int));\n    }\n\n    // Try with underscores removed\n    let normalized = trimmed.replace('_', \"\");\n    if let Ok(int) = normalized.parse::<i64>() {\n        return Ok(Value::Int(int));\n    }\n\n    // Try parsing as BigInt for values too large for i64\n    if let Ok(bi) = normalized.parse::<BigInt>() {\n        return Ok(LongInt::new(bi).into_value(heap)?);\n    }\n\n    Err(value_error_invalid_literal_for_int(value))\n}\n\n/// Creates the `ValueError` raised by `int()` when a string cannot be parsed.\n///\n/// Matches CPython's message format: `invalid literal for int() with base 10: '...'`.\nfn value_error_invalid_literal_for_int(value: &str) -> RunError {\n    SimpleException::new_msg(\n        ExcType::ValueError,\n        format!(\"invalid literal for int() with base 10: {}\", StringRepr(value)),\n    )\n    .into()\n}\n\n/// Dispatches a classmethod call on a type object.\n///\n/// Handles classmethods like `dict.fromkeys()` and `bytes.fromhex()` that are\n/// called on the type itself rather than on an instance.\npub(crate) fn call_type_method(\n    t: Type,\n    method_id: StringId,\n    args: ArgValues,\n    vm: &mut VM<'_, '_, impl ResourceTracker>,\n) -> Result<Value, RunError> {\n    match (t, method_id) {\n        (Type::Dict, m) if m == StaticStrings::Fromkeys => return dict_fromkeys(args, vm),\n        (Type::Bytes, m) if m == StaticStrings::Fromhex => {\n            return bytes_fromhex(args, vm);\n        }\n        _ => {}\n    }\n    // Other types or unknown methods - report actual type name, not 'type'\n    args.drop_with_heap(vm.heap);\n    Err(ExcType::attribute_error(t, vm.interns.get_str(method_id)))\n}\n"
  },
  {
    "path": "crates/monty/src/value.rs",
    "content": "use std::{\n    borrow::Cow,\n    cmp::Ordering,\n    collections::hash_map::DefaultHasher,\n    fmt::{self, Write},\n    hash::{Hash, Hasher},\n    mem::discriminant,\n    str::FromStr,\n};\n\nuse ahash::AHashSet;\nuse num_bigint::BigInt;\nuse num_integer::Integer;\nuse num_traits::{ToPrimitive, Zero};\n\nuse crate::{\n    asyncio::CallId,\n    builtins::Builtins,\n    bytecode::{CallResult, VM},\n    exception_private::{ExcType, RunError, RunResult, SimpleException},\n    heap::{ContainsHeap, Heap, HeapData, HeapGuard, HeapId},\n    heap_data::HeapDataMut,\n    intern::{BytesId, FunctionId, Interns, LongIntId, StaticStrings, StringId},\n    modules::ModuleFunctions,\n    resource::{ResourceError, ResourceTracker, check_div_size, check_lshift_size, check_pow_size, check_repeat_size},\n    types::{\n        LongInt, Property, PyTrait, Str, Type,\n        bytes::{bytes_repr_fmt, get_byte_at_index, get_bytes_slice},\n        path,\n        str::{allocate_char, get_char_at_index, get_str_slice, string_repr_fmt},\n    },\n};\n\n/// Primary value type representing Python objects at runtime.\n///\n/// This enum uses a hybrid design: small immediate values (Int, Bool, None) are stored\n/// inline, while heap-allocated values (List, Str, Dict, etc.) are stored in the arena\n/// and referenced via `Ref(HeapId)`.\n///\n/// NOTE: `Clone` is intentionally NOT derived. Use `clone_with_heap()` for heap values\n/// or `clone_immediate()` for immediate values only. Direct cloning via `.clone()` would\n/// bypass reference counting and cause memory leaks.\n///\n/// NOTE: it's important to keep this size small to minimize memory overhead!\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum Value {\n    // Immediate values (stored inline, no heap allocation)\n    Undefined,\n    Ellipsis,\n    None,\n    Bool(bool),\n    Int(i64),\n    Float(f64),\n    /// An interned string literal. The StringId references the string in the Interns table.\n    /// To get the actual string content, use `interns.get(string_id)`.\n    InternString(StringId),\n    /// An interned bytes literal. The BytesId references the bytes in the Interns table.\n    /// To get the actual bytes content, use `interns.get_bytes(bytes_id)`.\n    InternBytes(BytesId),\n    /// An interned long integer literal. The `LongIntId` references the `BigInt` in the Interns table.\n    /// Used for integer literals exceeding i64 range. Converted to heap-allocated `LongInt` on load.\n    InternLongInt(LongIntId),\n    /// A builtin function or exception type\n    Builtin(Builtins),\n    /// A function from a module (not a global builtin).\n    /// Module functions require importing a module to access (e.g., `asyncio.gather`).\n    ModuleFunction(ModuleFunctions),\n    /// A function defined in the module (not a closure, doesn't capture any variables)\n    DefFunction(FunctionId),\n    /// Reference to an external function defined on the host.\n    ///\n    /// The `StringId` stores the interned function name. When called, the VM yields\n    /// a `FrameExit::ExternalCall` with this `StringId` so the host can look up and\n    /// execute the function by name.\n    ExtFunction(StringId),\n    /// A marker value representing special objects like sys.stdout/stderr.\n    /// These exist but have minimal functionality in the sandboxed environment.\n    Marker(Marker),\n    /// A property descriptor that computes its value when accessed.\n    /// When retrieved via `py_getattr`, the property's getter is invoked.\n    Property(Property),\n    /// A pending external function call result.\n    ///\n    /// Created when the host calls `run_pending()` instead of `run(result)` for an\n    /// external function call. The CallId correlates with the call that created it.\n    /// When awaited, blocks the task until the host provides a result via `resume()`.\n    ///\n    /// ExternalFutures follow single-shot semantics like coroutines - awaiting an\n    /// already-awaited ExternalFuture raises RuntimeError.\n    ExternalFuture(CallId),\n\n    // Heap-allocated values (stored in arena)\n    Ref(HeapId),\n\n    /// Sentinel value indicating this Value was properly cleaned up via `drop_with_heap`.\n    /// Only exists when `ref-count-panic` feature is enabled. Used to verify reference counting\n    /// correctness - if a `Ref` variant is dropped without calling `drop_with_heap`, the\n    /// Drop impl will panic.\n    #[cfg(feature = \"ref-count-panic\")]\n    Dereferenced,\n}\n\n/// Drop implementation that panics if a `Ref` variant is dropped without calling `drop_with_heap`.\n/// This helps catch reference counting bugs during development/testing.\n/// Only enabled when the `ref-count-panic` feature is active.\n#[cfg(feature = \"ref-count-panic\")]\nimpl Drop for Value {\n    fn drop(&mut self) {\n        if let Self::Ref(id) = self {\n            panic!(\"Value::Ref({id:?}) dropped without calling drop_with_heap() - this is a reference counting bug\");\n        }\n    }\n}\n\nimpl From<bool> for Value {\n    fn from(v: bool) -> Self {\n        Self::Bool(v)\n    }\n}\n\nimpl PyTrait for Value {\n    fn py_type(&self, heap: &Heap<impl ResourceTracker>) -> Type {\n        match self {\n            Self::Undefined => panic!(\"Cannot get type of undefined value\"),\n            Self::Ellipsis => Type::Ellipsis,\n            Self::None => Type::NoneType,\n            Self::Bool(_) => Type::Bool,\n            Self::Int(_) | Self::InternLongInt(_) => Type::Int,\n            Self::Float(_) => Type::Float,\n            Self::InternString(_) => Type::Str,\n            Self::InternBytes(_) => Type::Bytes,\n            Self::Builtin(c) => c.py_type(),\n            Self::ModuleFunction(_) => Type::BuiltinFunction,\n            Self::DefFunction(_) | Self::ExtFunction(_) => Type::Function,\n            Self::Marker(m) => m.py_type(),\n            Self::Property(_) => Type::Property,\n            Self::ExternalFuture(_) => Type::Coroutine,\n            Self::Ref(id) => heap.get(*id).py_type(heap),\n            #[cfg(feature = \"ref-count-panic\")]\n            Self::Dereferenced => panic!(\"Cannot access Dereferenced object\"),\n        }\n    }\n\n    fn py_len(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Option<usize> {\n        match self {\n            // Count Unicode characters, not bytes, to match Python semantics\n            Self::InternString(string_id) => Some(vm.interns.get_str(*string_id).chars().count()),\n            Self::InternBytes(bytes_id) => Some(vm.interns.get_bytes(*bytes_id).len()),\n            Self::Ref(id) => vm.heap.get(*id).py_len(vm),\n            _ => None,\n        }\n    }\n\n    fn py_eq(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> Result<bool, ResourceError> {\n        let interns = vm.interns;\n        match (self, other) {\n            (Self::Undefined, _) => Ok(false),\n            (_, Self::Undefined) => Ok(false),\n            (Self::Int(v1), Self::Int(v2)) => Ok(v1 == v2),\n            (Self::Bool(v1), Self::Bool(v2)) => Ok(v1 == v2),\n            (Self::Bool(v1), Self::Int(v2)) => Ok(i64::from(*v1) == *v2),\n            (Self::Int(v1), Self::Bool(v2)) => Ok(*v1 == i64::from(*v2)),\n            (Self::Float(v1), Self::Float(v2)) => Ok(v1 == v2),\n            (Self::Int(v1), Self::Float(v2)) => Ok((*v1 as f64) == *v2),\n            (Self::Float(v1), Self::Int(v2)) => Ok(*v1 == (*v2 as f64)),\n            (Self::Bool(v1), Self::Float(v2)) => Ok((i64::from(*v1) as f64) == *v2),\n            (Self::Float(v1), Self::Bool(v2)) => Ok(*v1 == (i64::from(*v2) as f64)),\n            (Self::None, Self::None) => Ok(true),\n\n            // Int == LongInt comparison\n            (Self::Int(a), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    Ok(BigInt::from(*a) == *li.inner())\n                } else {\n                    Ok(false)\n                }\n            }\n            // LongInt == Int comparison\n            (Self::Ref(id), Self::Int(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    Ok(*li.inner() == BigInt::from(*b))\n                } else {\n                    Ok(false)\n                }\n            }\n\n            // For interned interns, compare by StringId first (fast path for same interned string)\n            (Self::InternString(s1), Self::InternString(s2)) => Ok(s1 == s2),\n            // for strings we need to account for the fact they might be either interned or not\n            (Self::InternString(string_id), Self::Ref(id2)) => {\n                if let HeapData::Str(s2) = vm.heap.get(*id2) {\n                    Ok(interns.get_str(*string_id) == s2.as_str())\n                } else {\n                    Ok(false)\n                }\n            }\n            (Self::Ref(id1), Self::InternString(string_id)) => {\n                if let HeapData::Str(s1) = vm.heap.get(*id1) {\n                    Ok(s1.as_str() == interns.get_str(*string_id))\n                } else {\n                    Ok(false)\n                }\n            }\n\n            // For interned bytes, compare by content (bytes are not deduplicated unlike interns)\n            (Self::InternBytes(b1), Self::InternBytes(b2)) => {\n                // Fast path: same BytesId means same content\n                Ok(b1 == b2 || interns.get_bytes(*b1) == interns.get_bytes(*b2))\n            }\n            // same for bytes\n            (Self::InternBytes(bytes_id), Self::Ref(id2)) => {\n                if let HeapData::Bytes(b2) = vm.heap.get(*id2) {\n                    Ok(interns.get_bytes(*bytes_id) == b2.as_slice())\n                } else {\n                    Ok(false)\n                }\n            }\n            (Self::Ref(id1), Self::InternBytes(bytes_id)) => {\n                if let HeapData::Bytes(b1) = vm.heap.get(*id1) {\n                    Ok(b1.as_slice() == interns.get_bytes(*bytes_id))\n                } else {\n                    Ok(false)\n                }\n            }\n\n            (Self::Ref(id1), Self::Ref(id2)) => {\n                if *id1 == *id2 {\n                    return Ok(true);\n                }\n                Heap::with_two(vm, *id1, *id2, |vm, left, right| left.py_eq(right, vm))\n            }\n\n            // Builtins equality - just check the enums are equal\n            (Self::Builtin(b1), Self::Builtin(b2)) => Ok(b1 == b2),\n            // Module functions equality\n            (Self::ModuleFunction(mf1), Self::ModuleFunction(mf2)) => Ok(mf1 == mf2),\n            (Self::DefFunction(f1), Self::DefFunction(f2)) => Ok(f1 == f2),\n            // Markers compare equal if they're the same variant\n            (Self::Marker(m1), Self::Marker(m2)) => Ok(m1 == m2),\n            // Properties compare equal if they're the same variant\n            (Self::Property(p1), Self::Property(p2)) => Ok(p1 == p2),\n\n            _ => Ok(false),\n        }\n    }\n\n    fn py_cmp(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Ordering>, ResourceError> {\n        let interns = vm.interns;\n        // py_cmp handles numbers, strings, bytes, and tuples.\n        // Recursion depth tracking for tuples is handled in Tuple::py_cmp.\n        match (self, other) {\n            (Self::Int(s), Self::Int(o)) => Ok(s.partial_cmp(o)),\n            (Self::Float(s), Self::Float(o)) => Ok(s.partial_cmp(o)),\n            (Self::Int(s), Self::Float(o)) => Ok((*s as f64).partial_cmp(o)),\n            (Self::Float(s), Self::Int(o)) => Ok(s.partial_cmp(&(*o as f64))),\n            // Bool promotion: convert to Int and re-dispatch. Recursion is bounded\n            // to at most 2 levels (Bool→Int, then Int matches directly above).\n            (Self::Bool(s), _) => Self::Int(i64::from(*s)).py_cmp(other, vm),\n            (_, Self::Bool(s)) => self.py_cmp(&Self::Int(i64::from(*s)), vm),\n            // Int vs LongInt comparison\n            (Self::Int(a), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    Ok(BigInt::from(*a).partial_cmp(li.inner()))\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt vs Int comparison\n            (Self::Ref(id), Self::Int(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    Ok(li.inner().partial_cmp(&BigInt::from(*b)))\n                } else {\n                    Ok(None)\n                }\n            }\n            // Ref vs Ref comparison: handles LongInt, Str, and Tuple\n            (Self::Ref(id1), Self::Ref(id2)) => match (vm.heap.get(*id1), vm.heap.get(*id2)) {\n                (HeapData::LongInt(a), HeapData::LongInt(b)) => Ok(a.inner().partial_cmp(b.inner())),\n                (HeapData::Str(a), HeapData::Str(b)) => Ok(a.as_str().partial_cmp(b.as_str())),\n                (HeapData::Tuple(_), HeapData::Tuple(_)) => {\n                    Heap::with_two(vm, *id1, *id2, |vm, left, right| left.py_cmp(right, vm))\n                }\n                _ => Ok(None),\n            },\n            // Interned string comparisons\n            (Self::InternString(s1), Self::InternString(s2)) => {\n                Ok(interns.get_str(*s1).partial_cmp(interns.get_str(*s2)))\n            }\n            // Cross-type string comparisons: interned vs heap-allocated\n            (Self::InternString(s1), Self::Ref(id2)) => {\n                if let HeapData::Str(s2) = vm.heap.get(*id2) {\n                    Ok(interns.get_str(*s1).partial_cmp(s2.as_str()))\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::Ref(id1), Self::InternString(s2)) => {\n                if let HeapData::Str(s1) = vm.heap.get(*id1) {\n                    Ok(s1.as_str().partial_cmp(interns.get_str(*s2)))\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::InternBytes(b1), Self::InternBytes(b2)) => {\n                Ok(interns.get_bytes(*b1).partial_cmp(interns.get_bytes(*b2)))\n            }\n            _ => Ok(None),\n        }\n    }\n\n    fn py_bool(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> bool {\n        match self {\n            Self::Undefined => false,\n            Self::Ellipsis => true,\n            Self::None => false,\n            Self::Bool(b) => *b,\n            Self::Int(v) => *v != 0,\n            Self::Float(f) => *f != 0.0,\n            // InternLongInt is always truthy (if it were zero, it would fit in i64)\n            Self::InternLongInt(_) => true,\n            Self::Builtin(_) | Self::ModuleFunction(_) => true, // Builtins are always truthy\n            Self::DefFunction(_) | Self::ExtFunction(_) => true, // Functions are always truthy\n            Self::Marker(_) => true,                            // Markers are always truthy\n            Self::Property(_) => true,                          // Properties are always truthy\n            Self::ExternalFuture(_) => true,                    // ExternalFutures are always truthy\n            Self::InternString(string_id) => !vm.interns.get_str(*string_id).is_empty(),\n            Self::InternBytes(bytes_id) => !vm.interns.get_bytes(*bytes_id).is_empty(),\n            Self::Ref(id) => vm.heap.get(*id).py_bool(vm),\n            #[cfg(feature = \"ref-count-panic\")]\n            Self::Dereferenced => panic!(\"Cannot access Dereferenced object\"),\n        }\n    }\n\n    fn py_repr_fmt(\n        &self,\n        f: &mut impl Write,\n        vm: &VM<'_, '_, impl ResourceTracker>,\n        heap_ids: &mut AHashSet<HeapId>,\n    ) -> std::fmt::Result {\n        let interns = vm.interns;\n        match self {\n            Self::Undefined => f.write_str(\"Undefined\"),\n            Self::Ellipsis => f.write_str(\"Ellipsis\"),\n            Self::None => f.write_str(\"None\"),\n            Self::Bool(true) => f.write_str(\"True\"),\n            Self::Bool(false) => f.write_str(\"False\"),\n            Self::Int(v) => write!(f, \"{v}\"),\n            Self::InternLongInt(long_int_id) => write!(f, \"{}\", interns.get_long_int(*long_int_id)),\n            Self::Float(v) => {\n                let s = v.to_string();\n                if s.contains('.') {\n                    f.write_str(&s)\n                } else {\n                    write!(f, \"{s}.0\")\n                }\n            }\n            Self::Builtin(b) => b.py_repr_fmt(f),\n            Self::ModuleFunction(mf) => mf.py_repr_fmt(f, self.id()),\n            Self::DefFunction(f_id) => interns.get_function(*f_id).py_repr_fmt(f, interns, self.id()),\n            Self::ExtFunction(name_id) => {\n                write!(f, \"<function '{}' external>\", interns.get_str(*name_id))\n            }\n            Self::InternString(string_id) => string_repr_fmt(interns.get_str(*string_id), f),\n            Self::InternBytes(bytes_id) => bytes_repr_fmt(interns.get_bytes(*bytes_id), f),\n            Self::Marker(m) => m.py_repr_fmt(f),\n            Self::Property(p) => write!(f, \"<property {p:?}>\"),\n            Self::ExternalFuture(call_id) => write!(f, \"<coroutine external_future({})>\", call_id.raw()),\n            Self::Ref(id) => {\n                if heap_ids.contains(id) {\n                    // Cycle detected - write type-specific placeholder following Python semantics\n                    match vm.heap.get(*id) {\n                        HeapData::List(_) => f.write_str(\"[...]\"),\n                        HeapData::Tuple(_) => f.write_str(\"(...)\"),\n                        HeapData::Dict(_) => f.write_str(\"{...}\"),\n                        // Other types don't typically have cycles, but handle gracefully\n                        _ => f.write_str(\"...\"),\n                    }\n                } else {\n                    heap_ids.insert(*id);\n                    let result = vm.heap.get(*id).py_repr_fmt(f, vm, heap_ids);\n                    heap_ids.remove(id);\n                    result\n                }\n            }\n            #[cfg(feature = \"ref-count-panic\")]\n            Self::Dereferenced => panic!(\"Cannot access Dereferenced object\"),\n        }\n    }\n\n    fn py_str(&self, vm: &VM<'_, '_, impl ResourceTracker>) -> Cow<'static, str> {\n        match self {\n            Self::InternString(string_id) => vm.interns.get_str(*string_id).to_owned().into(),\n            Self::Ref(id) => vm.heap.get(*id).py_str(vm),\n            _ => self.py_repr(vm),\n        }\n    }\n\n    fn py_add(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Value>, crate::resource::ResourceError> {\n        let interns = vm.interns;\n        match (self, other) {\n            // Int + Int with overflow detection\n            (Self::Int(a), Self::Int(b)) => {\n                if let Some(result) = a.checked_add(*b) {\n                    Ok(Some(Self::Int(result)))\n                } else {\n                    // Overflow - promote to LongInt\n                    let li = LongInt::from(*a) + LongInt::from(*b);\n                    li.into_value(vm.heap).map(Some)\n                }\n            }\n            // Int + LongInt\n            (Self::Int(i), Self::Ref(id)) | (Self::Ref(id), Self::Int(i)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    let result = LongInt::new(li.inner() + i);\n                    result.into_value(vm.heap).map(Some)\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::Float(v1), Self::Float(v2)) => Ok(Some(Self::Float(v1 + v2))),\n            // Int + Float and Float + Int\n            (Self::Int(a), Self::Float(b)) => Ok(Some(Self::Float(*a as f64 + b))),\n            (Self::Float(a), Self::Int(b)) => Ok(Some(Self::Float(a + *b as f64))),\n            (Self::Ref(id1), Self::Ref(id2)) => {\n                Heap::with_two(vm, *id1, *id2, |vm, left, right| left.py_add(right, vm))\n            }\n            (Self::InternString(s1), Self::InternString(s2)) => {\n                let concat = format!(\"{}{}\", interns.get_str(*s1), interns.get_str(*s2));\n                Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Str(concat.into()))?)))\n            }\n            // for strings we need to account for the fact they might be either interned or not\n            (Self::InternString(string_id), Self::Ref(id2)) => {\n                if let HeapData::Str(s2) = vm.heap.get(*id2) {\n                    let concat = format!(\"{}{}\", interns.get_str(*string_id), s2.as_str());\n                    Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Str(concat.into()))?)))\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::Ref(id1), Self::InternString(string_id)) => {\n                if let HeapData::Str(s1) = vm.heap.get(*id1) {\n                    let concat = format!(\"{}{}\", s1.as_str(), interns.get_str(*string_id));\n                    Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Str(concat.into()))?)))\n                } else {\n                    Ok(None)\n                }\n            }\n            // same for bytes\n            (Self::InternBytes(b1), Self::InternBytes(b2)) => {\n                let bytes1 = interns.get_bytes(*b1);\n                let bytes2 = interns.get_bytes(*b2);\n                let mut b = Vec::with_capacity(bytes1.len() + bytes2.len());\n                b.extend_from_slice(bytes1);\n                b.extend_from_slice(bytes2);\n                Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Bytes(b.into()))?)))\n            }\n            (Self::InternBytes(bytes_id), Self::Ref(id2)) => {\n                if let HeapData::Bytes(b2) = vm.heap.get(*id2) {\n                    let bytes1 = interns.get_bytes(*bytes_id);\n                    let mut b = Vec::with_capacity(bytes1.len() + b2.len());\n                    b.extend_from_slice(bytes1);\n                    b.extend_from_slice(b2);\n                    Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Bytes(b.into()))?)))\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::Ref(id1), Self::InternBytes(bytes_id)) => {\n                if let HeapData::Bytes(b1) = vm.heap.get(*id1) {\n                    let bytes2 = interns.get_bytes(*bytes_id);\n                    let mut b = Vec::with_capacity(b1.len() + bytes2.len());\n                    b.extend_from_slice(b1);\n                    b.extend_from_slice(bytes2);\n                    Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Bytes(b.into()))?)))\n                } else {\n                    Ok(None)\n                }\n            }\n            _ => Ok(None),\n        }\n    }\n\n    fn py_sub(\n        &self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> Result<Option<Self>, crate::resource::ResourceError> {\n        match (self, other) {\n            // Int - Int with overflow detection\n            (Self::Int(a), Self::Int(b)) => {\n                if let Some(result) = a.checked_sub(*b) {\n                    Ok(Some(Self::Int(result)))\n                } else {\n                    // Overflow - promote to LongInt\n                    let li = LongInt::from(*a) - LongInt::from(*b);\n                    li.into_value(vm.heap).map(Some)\n                }\n            }\n            // Int - LongInt\n            (Self::Int(a), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    let result = LongInt::from(*a) - LongInt::new(li.inner().clone());\n                    result.into_value(vm.heap).map(Some)\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt - Int\n            (Self::Ref(id), Self::Int(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    let result = LongInt::new(li.inner().clone()) - LongInt::from(*b);\n                    result.into_value(vm.heap).map(Some)\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt - LongInt\n            (Self::Ref(id1), Self::Ref(id2)) => {\n                Heap::with_two(vm, *id1, *id2, |vm, left, right| left.py_sub(right, vm))\n            }\n            // Float - Float\n            (Self::Float(a), Self::Float(b)) => Ok(Some(Self::Float(a - b))),\n            // Int - Float and Float - Int\n            (Self::Int(a), Self::Float(b)) => Ok(Some(Self::Float(*a as f64 - b))),\n            (Self::Float(a), Self::Int(b)) => Ok(Some(Self::Float(a - *b as f64))),\n            _ => Ok(None),\n        }\n    }\n\n    fn py_mod(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Self>> {\n        match (self, other) {\n            (Self::Int(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else if let Some(r) = a.checked_rem(*b) {\n                    // Python modulo: result has the same sign as divisor (b)\n                    let result = if r != 0 && (*a < 0) != (*b < 0) { r + *b } else { r };\n                    Ok(Some(Self::Int(result)))\n                } else {\n                    // Overflow - i64::MIN % -1 is 0\n                    Ok(Some(Self::Int(0)))\n                }\n            }\n            // Int % LongInt\n            (Self::Int(a), Self::Ref(id)) => {\n                // Clone to avoid borrow conflict with heap mutation\n                let b_clone = if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if li.is_zero() {\n                        return Err(ExcType::zero_division().into());\n                    }\n                    li.inner().clone()\n                } else {\n                    return Ok(None);\n                };\n                let bi = BigInt::from(*a).mod_floor(&b_clone);\n                Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n            }\n            // LongInt % Int\n            (Self::Ref(id), Self::Int(b)) => {\n                if *b == 0 {\n                    return Err(ExcType::zero_division().into());\n                }\n                // Clone to avoid borrow conflict with heap mutation\n                let a_clone = if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    li.inner().clone()\n                } else {\n                    return Ok(None);\n                };\n                let bi = a_clone.mod_floor(&BigInt::from(*b));\n                Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n            }\n            // LongInt % LongInt\n            (Self::Ref(id1), Self::Ref(id2)) => {\n                Heap::with_two(vm, *id1, *id2, |vm, left, right| left.py_mod(right, vm))\n            }\n            (Self::Float(v1), Self::Float(v2)) => {\n                if *v2 == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(v1 % v2)))\n                }\n            }\n            (Self::Float(v1), Self::Int(v2)) => {\n                if *v2 == 0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(v1 % (*v2 as f64))))\n                }\n            }\n            (Self::Int(v1), Self::Float(v2)) => {\n                if *v2 == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float((*v1 as f64) % v2)))\n                }\n            }\n            _ => Ok(None),\n        }\n    }\n\n    fn py_mod_eq(&self, other: &Self, right_value: i64) -> Option<bool> {\n        match (self, other) {\n            (Self::Int(v1), Self::Int(v2)) => {\n                if let Some(r) = v1.checked_rem(*v2) {\n                    // Python modulo: result has same sign as divisor\n                    let result = if r != 0 && (*v1 < 0) != (*v2 < 0) { r + *v2 } else { r };\n                    Some(result == right_value)\n                } else {\n                    // checked_rem returns None for overflow (i64::MIN % -1) or zero division\n                    (*v2 != 0).then_some(0 == right_value)\n                }\n            }\n            (Self::Float(v1), Self::Float(v2)) => Some(v1 % v2 == right_value as f64),\n            (Self::Float(v1), Self::Int(v2)) => Some(v1 % (*v2 as f64) == right_value as f64),\n            (Self::Int(v1), Self::Float(v2)) => Some((*v1 as f64) % v2 == right_value as f64),\n            _ => None,\n        }\n    }\n\n    fn py_iadd(\n        &mut self,\n        other: &Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n        _self_id: Option<HeapId>,\n    ) -> Result<bool, crate::resource::ResourceError> {\n        let interns = vm.interns;\n        match (&self, other) {\n            (Self::Int(v1), Self::Int(v2)) => {\n                if let Some(result) = v1.checked_add(*v2) {\n                    *self = Self::Int(result);\n                } else {\n                    // Overflow - promote to LongInt\n                    let li = LongInt::from(*v1) + LongInt::from(*v2);\n                    *self = li.into_value(vm.heap)?;\n                }\n                Ok(true)\n            }\n            (Self::Float(v1), Self::Float(v2)) => {\n                *self = Self::Float(*v1 + *v2);\n                Ok(true)\n            }\n            (Self::InternString(s1), Self::InternString(s2)) => {\n                let concat = format!(\"{}{}\", interns.get_str(*s1), interns.get_str(*s2));\n                *self = Self::Ref(vm.heap.allocate(HeapData::Str(concat.into()))?);\n                Ok(true)\n            }\n            (Self::InternString(string_id), Self::Ref(id2)) => {\n                let result = if let HeapData::Str(s2) = vm.heap.get(*id2) {\n                    let concat = format!(\"{}{}\", interns.get_str(*string_id), s2.as_str());\n                    *self = Self::Ref(vm.heap.allocate(HeapData::Str(concat.into()))?);\n                    true\n                } else {\n                    false\n                };\n                Ok(result)\n            }\n            // same for bytes\n            (Self::InternBytes(b1), Self::InternBytes(b2)) => {\n                let bytes1 = interns.get_bytes(*b1);\n                let bytes2 = interns.get_bytes(*b2);\n                let mut b = Vec::with_capacity(bytes1.len() + bytes2.len());\n                b.extend_from_slice(bytes1);\n                b.extend_from_slice(bytes2);\n                *self = Self::Ref(vm.heap.allocate(HeapData::Bytes(b.into()))?);\n                Ok(true)\n            }\n            (Self::InternBytes(bytes_id), Self::Ref(id2)) => {\n                let result = if let HeapData::Bytes(b2) = vm.heap.get(*id2) {\n                    let bytes1 = interns.get_bytes(*bytes_id);\n                    let mut b = Vec::with_capacity(bytes1.len() + b2.len());\n                    b.extend_from_slice(bytes1);\n                    b.extend_from_slice(b2);\n                    *self = Self::Ref(vm.heap.allocate(HeapData::Bytes(b.into()))?);\n                    true\n                } else {\n                    false\n                };\n                Ok(result)\n            }\n            (Self::Ref(id), Self::Ref(_)) => {\n                Heap::with_entry_mut(vm, *id, |vm, mut data| data.py_iadd(other, vm, Some(*id)))\n            }\n            _ => Ok(false),\n        }\n    }\n\n    fn py_mult(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        let interns = vm.interns;\n        match (self, other) {\n            // Numeric multiplication with overflow promotion to LongInt\n            (Self::Int(a), Self::Int(b)) => {\n                if let Some(result) = a.checked_mul(*b) {\n                    Ok(Some(Self::Int(result)))\n                } else {\n                    // Overflow - promote to LongInt\n                    let li = LongInt::from(*a) * LongInt::from(*b);\n                    Ok(Some(li.into_value(vm.heap)?))\n                }\n            }\n            // Int * Ref (LongInt or sequence)\n            (Self::Int(a), Self::Ref(id)) => vm.heap.mult_ref_by_i64(*id, *a),\n            // Ref * Int (LongInt or sequence)\n            (Self::Ref(id), Self::Int(b)) => vm.heap.mult_ref_by_i64(*id, *b),\n            // Ref * Ref (LongInt * LongInt, sequence * LongInt, etc.)\n            (Self::Ref(id1), Self::Ref(id2)) => vm.heap.mult_heap_values(*id1, *id2),\n            (Self::Float(a), Self::Float(b)) => Ok(Some(Self::Float(a * b))),\n            (Self::Int(a), Self::Float(b)) => Ok(Some(Self::Float(*a as f64 * b))),\n            (Self::Float(a), Self::Int(b)) => Ok(Some(Self::Float(a * *b as f64))),\n\n            // Bool numeric multiplication (True=1, False=0)\n            (Self::Bool(a), Self::Int(b)) => {\n                let a_int = i64::from(*a);\n                Ok(Some(Self::Int(a_int * b)))\n            }\n            (Self::Int(a), Self::Bool(b)) => {\n                let b_int = i64::from(*b);\n                Ok(Some(Self::Int(a * b_int)))\n            }\n            (Self::Bool(a), Self::Float(b)) => {\n                let a_float = if *a { 1.0 } else { 0.0 };\n                Ok(Some(Self::Float(a_float * b)))\n            }\n            (Self::Float(a), Self::Bool(b)) => {\n                let b_float = if *b { 1.0 } else { 0.0 };\n                Ok(Some(Self::Float(a * b_float)))\n            }\n            (Self::Bool(a), Self::Bool(b)) => {\n                let result = i64::from(*a) * i64::from(*b);\n                Ok(Some(Self::Int(result)))\n            }\n\n            // String repetition: \"ab\" * 3 or 3 * \"ab\"\n            (Self::InternString(s), Self::Int(n)) | (Self::Int(n), Self::InternString(s)) => {\n                let count = i64_to_repeat_count(*n)?;\n                let str_ref = interns.get_str(*s);\n                check_repeat_size(str_ref.len(), count, vm.heap.tracker())?;\n                let result = str_ref.repeat(count);\n                Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Str(result.into()))?)))\n            }\n\n            // Bytes repetition: b\"ab\" * 3 or 3 * b\"ab\"\n            (Self::InternBytes(b), Self::Int(n)) | (Self::Int(n), Self::InternBytes(b)) => {\n                let count = i64_to_repeat_count(*n)?;\n                let bytes_ref = interns.get_bytes(*b);\n                check_repeat_size(bytes_ref.len(), count, vm.heap.tracker())?;\n                let result: Vec<u8> = bytes_ref.repeat(count);\n                Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Bytes(result.into()))?)))\n            }\n\n            // String repetition with LongInt: \"ab\" * bigint or bigint * \"ab\"\n            (Self::InternString(s), Self::Ref(id)) | (Self::Ref(id), Self::InternString(s)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    let count = longint_to_repeat_count(li)?;\n                    let str_ref = interns.get_str(*s);\n                    check_repeat_size(str_ref.len(), count, vm.heap.tracker())?;\n                    let result = str_ref.repeat(count);\n                    Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Str(result.into()))?)))\n                } else {\n                    Ok(None)\n                }\n            }\n\n            // Bytes repetition with LongInt: b\"ab\" * bigint or bigint * b\"ab\"\n            (Self::InternBytes(b), Self::Ref(id)) | (Self::Ref(id), Self::InternBytes(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    let count = longint_to_repeat_count(li)?;\n                    let bytes_ref = interns.get_bytes(*b);\n                    check_repeat_size(bytes_ref.len(), count, vm.heap.tracker())?;\n                    let result: Vec<u8> = bytes_ref.repeat(count);\n                    Ok(Some(Self::Ref(vm.heap.allocate(HeapData::Bytes(result.into()))?)))\n                } else {\n                    Ok(None)\n                }\n            }\n\n            _ => Ok(None),\n        }\n    }\n\n    fn py_div(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        let interns = vm.interns;\n        match (self, other) {\n            // True division always returns float\n            (Self::Int(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(*a as f64 / *b as f64)))\n                }\n            }\n            // Int / LongInt\n            (Self::Int(a), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if li.is_zero() {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        // Convert both to f64 for division\n                        let a_f64 = *a as f64;\n                        let b_f64 = li.to_f64().unwrap_or(f64::INFINITY);\n                        Ok(Some(Self::Float(a_f64 / b_f64)))\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt / Int\n            (Self::Ref(id), Self::Int(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if *b == 0 {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        // Convert both to f64 for division\n                        let a_f64 = li.to_f64().unwrap_or(f64::INFINITY);\n                        let b_f64 = *b as f64;\n                        Ok(Some(Self::Float(a_f64 / b_f64)))\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt / LongInt\n            (Self::Ref(id1), Self::Ref(id2)) => match (vm.heap.get(*id1), vm.heap.get(*id2)) {\n                (HeapData::LongInt(li1), HeapData::LongInt(li2)) => {\n                    if li2.is_zero() {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        let a_f64 = li1.to_f64().unwrap_or(f64::INFINITY);\n                        let b_f64 = li2.to_f64().unwrap_or(f64::INFINITY);\n                        Ok(Some(Self::Float(a_f64 / b_f64)))\n                    }\n                }\n                _ => Ok(None),\n            },\n            // LongInt / Float\n            (Self::Ref(id), Self::Float(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if *b == 0.0 {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        let a_f64 = li.to_f64().unwrap_or(f64::INFINITY);\n                        Ok(Some(Self::Float(a_f64 / b)))\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            // Float / LongInt\n            (Self::Float(a), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if li.is_zero() {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        let b_f64 = li.to_f64().unwrap_or(f64::INFINITY);\n                        Ok(Some(Self::Float(a / b_f64)))\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::Float(a), Self::Float(b)) => {\n                if *b == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(a / b)))\n                }\n            }\n            (Self::Int(a), Self::Float(b)) => {\n                if *b == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(*a as f64 / b)))\n                }\n            }\n            (Self::Float(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(a / *b as f64)))\n                }\n            }\n            // Bool division (True=1, False=0)\n            (Self::Bool(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(f64::from(*a) / *b as f64)))\n                }\n            }\n            (Self::Int(a), Self::Bool(b)) => {\n                if *b {\n                    Ok(Some(Self::Float(*a as f64))) // a / 1 = a\n                } else {\n                    Err(ExcType::zero_division().into())\n                }\n            }\n            (Self::Bool(a), Self::Float(b)) => {\n                if *b == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float(f64::from(*a) / b)))\n                }\n            }\n            (Self::Float(a), Self::Bool(b)) => {\n                if *b {\n                    Ok(Some(Self::Float(*a))) // a / 1.0 = a\n                } else {\n                    Err(ExcType::zero_division().into())\n                }\n            }\n            (Self::Bool(a), Self::Bool(b)) => {\n                if *b {\n                    Ok(Some(Self::Float(f64::from(*a)))) // a / 1 = a\n                } else {\n                    Err(ExcType::zero_division().into())\n                }\n            }\n            _ => {\n                // Check for Path / (str or Path) - path concatenation\n                if let Self::Ref(id) = self\n                    && matches!(vm.heap.get(*id), HeapData::Path(_))\n                {\n                    return path::path_div(*id, other, vm.heap, interns);\n                }\n                Ok(None)\n            }\n        }\n    }\n\n    fn py_floordiv(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        match (self, other) {\n            // Floor division: int // int returns int\n            (Self::Int(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else if let Some((d, _)) = floor_divmod(*a, *b) {\n                    Ok(Some(Self::Int(d)))\n                } else {\n                    // Overflow - promote to LongInt\n                    check_div_size(i64_bits(*a), vm.heap.tracker())?;\n                    let bi = BigInt::from(*a).div_floor(&BigInt::from(*b));\n                    Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                }\n            }\n            // Int // LongInt\n            (Self::Int(a), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if li.is_zero() {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        let bi = BigInt::from(*a).div_floor(li.inner());\n                        Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt // Int\n            (Self::Ref(id), Self::Int(b)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if *b == 0 {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        let bi = li.inner().div_floor(&BigInt::from(*b));\n                        Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            // LongInt // LongInt\n            (Self::Ref(id1), Self::Ref(id2)) => match (vm.heap.get(*id1), vm.heap.get(*id2)) {\n                (HeapData::LongInt(li1), HeapData::LongInt(li2)) => {\n                    if li2.is_zero() {\n                        Err(ExcType::zero_division().into())\n                    } else {\n                        let bi = li1.inner().div_floor(li2.inner());\n                        Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                    }\n                }\n                _ => Ok(None),\n            },\n            // Float floor division returns float\n            (Self::Float(a), Self::Float(b)) => {\n                if *b == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float((a / b).floor())))\n                }\n            }\n            (Self::Int(a), Self::Float(b)) => {\n                if *b == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float((*a as f64 / b).floor())))\n                }\n            }\n            (Self::Float(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float((a / *b as f64).floor())))\n                }\n            }\n            // Bool floor division (True=1, False=0)\n            (Self::Bool(a), Self::Int(b)) => {\n                if *b == 0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    let a_int = i64::from(*a);\n                    // Use same floor division logic as Int // Int\n                    let d = a_int / b;\n                    let r = a_int % b;\n                    let result = if r != 0 && (a_int < 0) != (*b < 0) { d - 1 } else { d };\n                    Ok(Some(Self::Int(result)))\n                }\n            }\n            (Self::Int(a), Self::Bool(b)) => {\n                if *b {\n                    Ok(Some(Self::Int(*a))) // a // 1 = a\n                } else {\n                    Err(ExcType::zero_division().into())\n                }\n            }\n            (Self::Bool(a), Self::Float(b)) => {\n                if *b == 0.0 {\n                    Err(ExcType::zero_division().into())\n                } else {\n                    Ok(Some(Self::Float((f64::from(*a) / b).floor())))\n                }\n            }\n            (Self::Float(a), Self::Bool(b)) => {\n                if *b {\n                    Ok(Some(Self::Float(a.floor()))) // a // 1.0 = floor(a)\n                } else {\n                    Err(ExcType::zero_division().into())\n                }\n            }\n            (Self::Bool(a), Self::Bool(b)) => {\n                if *b {\n                    Ok(Some(Self::Int(i64::from(*a)))) // a // 1 = a\n                } else {\n                    Err(ExcType::zero_division().into())\n                }\n            }\n            _ => Ok(None),\n        }\n    }\n\n    fn py_pow(&self, other: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Option<Value>> {\n        match (self, other) {\n            (Self::Int(base), Self::Int(exp)) => {\n                if *base == 0 && *exp < 0 {\n                    Err(ExcType::zero_negative_power())\n                } else if *exp >= 0 {\n                    // Positive exponent: try to return int, promote to LongInt on overflow\n                    if let Ok(exp_u32) = u32::try_from(*exp) {\n                        if let Some(result) = base.checked_pow(exp_u32) {\n                            Ok(Some(Self::Int(result)))\n                        } else {\n                            // Overflow - promote to LongInt\n                            // Check size before computing to prevent DoS\n                            check_pow_size(i64_bits(*base), u64::from(exp_u32), vm.heap.tracker())?;\n                            let bi = BigInt::from(*base).pow(exp_u32);\n                            Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                        }\n                    } else {\n                        // exp > u32::MAX - use BigInt with modpow-style exponentiation\n                        // For very large exponents, we still need LongInt\n                        // Safety: exp >= 0 is guaranteed by the outer if condition\n                        #[expect(clippy::cast_sign_loss)]\n                        let exp_u64 = *exp as u64;\n                        // Check size before computing to prevent DoS\n                        check_pow_size(i64_bits(*base), exp_u64, vm.heap.tracker())?;\n                        let bi = bigint_pow(BigInt::from(*base), exp_u64);\n                        Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                    }\n                } else {\n                    // Negative exponent: return float\n                    // Use powi if exp fits in i32, otherwise use powf\n                    if let Ok(exp_i32) = i32::try_from(*exp) {\n                        Ok(Some(Self::Float((*base as f64).powi(exp_i32))))\n                    } else {\n                        Ok(Some(Self::Float((*base as f64).powf(*exp as f64))))\n                    }\n                }\n            }\n            // LongInt ** Int\n            (Self::Ref(id), Self::Int(exp)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if li.is_zero() && *exp < 0 {\n                        Err(ExcType::zero_negative_power())\n                    } else if *exp >= 0 {\n                        // Use BigInt pow for positive exponents\n                        if let Ok(exp_u32) = u32::try_from(*exp) {\n                            // Check size before computing to prevent DoS\n                            check_pow_size(li.bits(), u64::from(exp_u32), vm.heap.tracker())?;\n                            let bi = li.inner().pow(exp_u32);\n                            Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                        } else {\n                            // Safety: exp >= 0 is guaranteed by the outer if condition\n                            #[expect(clippy::cast_sign_loss)]\n                            let exp_u64 = *exp as u64;\n                            // Check size before computing to prevent DoS\n                            check_pow_size(li.bits(), exp_u64, vm.heap.tracker())?;\n                            let bi = bigint_pow(li.inner().clone(), exp_u64);\n                            Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                        }\n                    } else {\n                        // Negative exponent: return float (LongInt base becomes 0.0 for large values)\n                        if let Some(base_f64) = li.to_f64() {\n                            if let Ok(exp_i32) = i32::try_from(*exp) {\n                                Ok(Some(Self::Float(base_f64.powi(exp_i32))))\n                            } else {\n                                Ok(Some(Self::Float(base_f64.powf(*exp as f64))))\n                            }\n                        } else {\n                            // Base too large for f64, result approaches 0\n                            Ok(Some(Self::Float(0.0)))\n                        }\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            // Int ** LongInt (only small positive exponents make sense)\n            (Self::Int(base), Self::Ref(id)) => {\n                if let HeapData::LongInt(li) = vm.heap.get(*id) {\n                    if *base == 0 && li.is_negative() {\n                        Err(ExcType::zero_negative_power())\n                    } else if !li.is_negative() {\n                        // For very large exponents, most results are huge or 0/1\n                        // Check for x ** 0 = 1 first (including 0 ** 0 = 1)\n                        if li.is_zero() {\n                            Ok(Some(Self::Int(1)))\n                        } else if *base == 0 {\n                            Ok(Some(Self::Int(0)))\n                        } else if *base == 1 {\n                            Ok(Some(Self::Int(1)))\n                        } else if *base == -1 {\n                            // (-1) ** n = 1 if n is even, -1 if n is odd\n                            let is_even = (li.inner() % 2i32).is_zero();\n                            Ok(Some(Self::Int(if is_even { 1 } else { -1 })))\n                        } else if let Some(exp_u32) = li.to_u32() {\n                            // Reasonable exponent size\n                            if let Some(result) = base.checked_pow(exp_u32) {\n                                Ok(Some(Self::Int(result)))\n                            } else {\n                                // Check size before computing to prevent DoS\n                                check_pow_size(i64_bits(*base), u64::from(exp_u32), vm.heap.tracker())?;\n                                let bi = BigInt::from(*base).pow(exp_u32);\n                                Ok(Some(LongInt::new(bi).into_value(vm.heap)?))\n                            }\n                        } else {\n                            // Exponent too large - result would be astronomically large\n                            // Python handles this, but it would take forever. Use OverflowError\n                            Err(SimpleException::new_msg(ExcType::OverflowError, \"exponent too large\").into())\n                        }\n                    } else {\n                        // Negative LongInt exponent: return float\n                        if let (Some(base_f64), Some(exp_f64)) = (Some(*base as f64), li.to_f64()) {\n                            Ok(Some(Self::Float(base_f64.powf(exp_f64))))\n                        } else {\n                            Ok(Some(Self::Float(0.0)))\n                        }\n                    }\n                } else {\n                    Ok(None)\n                }\n            }\n            (Self::Float(base), Self::Float(exp)) => {\n                if *base == 0.0 && *exp < 0.0 {\n                    Err(ExcType::zero_negative_power())\n                } else {\n                    Ok(Some(Self::Float(base.powf(*exp))))\n                }\n            }\n            (Self::Int(base), Self::Float(exp)) => {\n                if *base == 0 && *exp < 0.0 {\n                    Err(ExcType::zero_negative_power())\n                } else {\n                    Ok(Some(Self::Float((*base as f64).powf(*exp))))\n                }\n            }\n            (Self::Float(base), Self::Int(exp)) => {\n                if *base == 0.0 && *exp < 0 {\n                    Err(ExcType::zero_negative_power())\n                } else if let Ok(exp_i32) = i32::try_from(*exp) {\n                    // Use powi if exp fits in i32\n                    Ok(Some(Self::Float(base.powi(exp_i32))))\n                } else {\n                    // Fall back to powf for exponents outside i32 range\n                    Ok(Some(Self::Float(base.powf(*exp as f64))))\n                }\n            }\n            // Bool power operations (True=1, False=0)\n            (Self::Bool(base), Self::Int(exp)) => {\n                let base_int = i64::from(*base);\n                if base_int == 0 && *exp < 0 {\n                    Err(ExcType::zero_negative_power())\n                } else if *exp >= 0 {\n                    // Positive exponent: 1**n=1, 0**n=0 (for n>0), 0**0=1\n                    if let Ok(exp_u32) = u32::try_from(*exp) {\n                        match base_int.checked_pow(exp_u32) {\n                            Some(result) => Ok(Some(Self::Int(result))),\n                            None => Ok(Some(Self::Float((base_int as f64).powf(*exp as f64)))),\n                        }\n                    } else {\n                        Ok(Some(Self::Float((base_int as f64).powf(*exp as f64))))\n                    }\n                } else {\n                    // Negative exponent: return float (1**-n=1.0)\n                    if let Ok(exp_i32) = i32::try_from(*exp) {\n                        Ok(Some(Self::Float((base_int as f64).powi(exp_i32))))\n                    } else {\n                        Ok(Some(Self::Float((base_int as f64).powf(*exp as f64))))\n                    }\n                }\n            }\n            (Self::Int(base), Self::Bool(exp)) => {\n                // n ** True = n, n ** False = 1\n                if *exp {\n                    Ok(Some(Self::Int(*base)))\n                } else {\n                    Ok(Some(Self::Int(1)))\n                }\n            }\n            (Self::Bool(base), Self::Float(exp)) => {\n                let base_float = f64::from(*base);\n                if base_float == 0.0 && *exp < 0.0 {\n                    Err(ExcType::zero_negative_power())\n                } else {\n                    Ok(Some(Self::Float(base_float.powf(*exp))))\n                }\n            }\n            (Self::Float(base), Self::Bool(exp)) => {\n                // base ** True = base, base ** False = 1.0\n                if *exp {\n                    Ok(Some(Self::Float(*base)))\n                } else {\n                    Ok(Some(Self::Float(1.0)))\n                }\n            }\n            (Self::Bool(base), Self::Bool(exp)) => {\n                // True ** True = 1, True ** False = 1, False ** True = 0, False ** False = 1\n                let base_int = i64::from(*base);\n                let exp_int = i64::from(*exp);\n                if exp_int == 0 {\n                    Ok(Some(Self::Int(1))) // anything ** 0 = 1\n                } else {\n                    Ok(Some(Self::Int(base_int))) // base ** 1 = base\n                }\n            }\n            _ => Ok(None),\n        }\n    }\n\n    fn py_getitem(&self, key: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<Self> {\n        let interns = vm.interns;\n        match self {\n            Self::Ref(id) => Heap::with_entry_mut(vm, *id, |vm, data| data.py_getitem(key, vm)),\n            Self::InternString(string_id) => {\n                // Check for slice first\n                if let Self::Ref(key_id) = key\n                    && let HeapData::Slice(slice_obj) = vm.heap.get(*key_id)\n                {\n                    let s = interns.get_str(*string_id);\n                    let char_count = s.chars().count();\n                    let (start, stop, step) = slice_obj\n                        .indices(char_count)\n                        .map_err(|()| ExcType::value_error_slice_step_zero())?;\n                    let result_str = get_str_slice(s, start, stop, step);\n                    let heap_id = vm.heap.allocate(HeapData::Str(Str::from(result_str)))?;\n                    return Ok(Self::Ref(heap_id));\n                }\n\n                // Handle interned string indexing, accepting Int and Bool\n                let index = match key {\n                    Self::Int(i) => *i,\n                    Self::Bool(b) => i64::from(*b),\n                    _ => return Err(ExcType::type_error_indices(Type::Str, key.py_type(vm.heap))),\n                };\n\n                let s = interns.get_str(*string_id);\n                let c = get_char_at_index(s, index).ok_or_else(ExcType::str_index_error)?;\n                Ok(allocate_char(c, vm.heap)?)\n            }\n            Self::InternBytes(bytes_id) => {\n                // Check for slice first\n                if let Self::Ref(key_id) = key\n                    && let HeapData::Slice(slice_obj) = vm.heap.get(*key_id)\n                {\n                    let bytes = interns.get_bytes(*bytes_id);\n                    let (start, stop, step) = slice_obj\n                        .indices(bytes.len())\n                        .map_err(|()| ExcType::value_error_slice_step_zero())?;\n                    let result_bytes = get_bytes_slice(bytes, start, stop, step);\n                    let heap_id = vm\n                        .heap\n                        .allocate(HeapData::Bytes(crate::types::Bytes::new(result_bytes)))?;\n                    return Ok(Self::Ref(heap_id));\n                }\n\n                // Handle interned bytes indexing - returns integer byte value\n                let index = match key {\n                    Self::Int(i) => *i,\n                    Self::Bool(b) => i64::from(*b),\n                    _ => return Err(ExcType::type_error_indices(Type::Bytes, key.py_type(vm.heap))),\n                };\n\n                let bytes = interns.get_bytes(*bytes_id);\n                let byte = get_byte_at_index(bytes, index).ok_or_else(ExcType::bytes_index_error)?;\n                Ok(Self::Int(i64::from(byte)))\n            }\n            _ => Err(ExcType::type_error_not_sub(self.py_type(vm.heap))),\n        }\n    }\n\n    fn py_setitem(&mut self, key: Self, value: Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<()> {\n        match self {\n            Self::Ref(id) => Heap::with_entry_mut(vm, *id, |vm, mut data| data.py_setitem(key, value, vm)),\n            _ => Err(ExcType::type_error(format!(\n                \"'{}' object does not support item assignment\",\n                self.py_type(vm.heap)\n            ))),\n        }\n    }\n}\n\nimpl Value {\n    /// Returns a stable, unique identifier for this value.\n    ///\n    /// Should match Python's `id()` function conceptually.\n    ///\n    /// For immediate values (Int, Float, Builtins), this computes a deterministic ID\n    /// based on the value's hash, avoiding heap allocation. This means `id(5) == id(5)` will\n    /// return True (unlike CPython for large integers outside the interning range).\n    ///\n    /// Singletons (None, True, False, etc.) return IDs from a dedicated tagged range.\n    /// Interned strings/bytes use their interner index for stable identity.\n    /// Heap-allocated values (Ref) reuse their `HeapId` inside the heap-tagged range.\n    pub fn id(&self) -> usize {\n        match self {\n            // Singletons have fixed tagged IDs\n            Self::Undefined => singleton_id(SingletonSlot::Undefined),\n            Self::Ellipsis => singleton_id(SingletonSlot::Ellipsis),\n            Self::None => singleton_id(SingletonSlot::None),\n            Self::Bool(b) => {\n                if *b {\n                    singleton_id(SingletonSlot::True)\n                } else {\n                    singleton_id(SingletonSlot::False)\n                }\n            }\n            // Interned strings/bytes/bigints use their index directly - the index is the stable identifier\n            Self::InternString(string_id) => INTERN_STR_ID_TAG | (string_id.index() & INTERN_STR_ID_MASK),\n            Self::InternBytes(bytes_id) => INTERN_BYTES_ID_TAG | (bytes_id.index() & INTERN_BYTES_ID_MASK),\n            Self::InternLongInt(long_int_id) => {\n                INTERN_LONG_INT_ID_TAG | (long_int_id.index() & INTERN_LONG_INT_ID_MASK)\n            }\n            // Already heap-allocated (includes Range and Exception), return id within a dedicated tag range\n            Self::Ref(id) => heap_tagged_id(*id),\n            // Value-based IDs for immediate types (no heap allocation!)\n            Self::Int(v) => int_value_id(*v),\n            Self::Float(v) => float_value_id(*v),\n            Self::Builtin(c) => builtin_value_id(*c),\n            Self::ModuleFunction(mf) => module_function_value_id(*mf),\n            Self::DefFunction(f_id) => function_value_id(*f_id),\n            Self::ExtFunction(name_id) => ext_function_value_id(*name_id),\n            // Markers get deterministic IDs based on discriminant\n            Self::Marker(m) => marker_value_id(*m),\n            // Properties get deterministic IDs based on discriminant\n            Self::Property(p) => property_value_id(*p),\n            // ExternalFutures get IDs based on their call_id\n            Self::ExternalFuture(call_id) => external_future_value_id(*call_id),\n            #[cfg(feature = \"ref-count-panic\")]\n            Self::Dereferenced => panic!(\"Cannot get id of Dereferenced object\"),\n        }\n    }\n\n    /// Returns the Ref ID if this value is a reference, otherwise returns None.\n    pub fn ref_id(&self) -> Option<HeapId> {\n        match self {\n            Self::Ref(id) => Some(*id),\n            _ => None,\n        }\n    }\n\n    /// Returns the module name if this value is a module, otherwise returns \"<unknown>\".\n    ///\n    /// Used for error messages in `from module import name` when the name doesn't exist.\n    pub fn module_name(&self, heap: &Heap<impl ResourceTracker>, interns: &Interns) -> String {\n        match self {\n            Self::Ref(id) => match heap.get(*id) {\n                HeapData::Module(module) => interns.get_str(module.name()).to_string(),\n                _ => \"<unknown>\".to_string(),\n            },\n            _ => \"<unknown>\".to_string(),\n        }\n    }\n\n    /// Equivalent of Python's `is` operator.\n    ///\n    /// Compares value identity by comparing their IDs.\n    pub fn is(&self, other: &Self) -> bool {\n        self.id() == other.id()\n    }\n\n    /// Computes the hash value for this value, used for dict keys.\n    ///\n    /// Returns `Ok(Some(hash))` for hashable types (immediate values and immutable heap types).\n    /// Returns `Ok(None)` for unhashable types (list, dict).\n    /// Returns `Err(ResourceError::Recursion)` if the recursion limit is exceeded\n    /// while hashing deeply nested containers (e.g., tuples of tuples).\n    ///\n    /// For heap-allocated values (Ref variant), this computes the hash lazily\n    /// on first use and caches it for subsequent calls.\n    ///\n    /// The `interns` parameter is needed for InternString/InternBytes to look up\n    /// their actual content and hash it consistently with equivalent heap Str/Bytes.\n    pub fn py_hash(\n        &self,\n        heap: &mut Heap<impl ResourceTracker>,\n        interns: &Interns,\n    ) -> Result<Option<u64>, ResourceError> {\n        // strings bytes bigints and heap allocated values have their own hashing logic\n        match self {\n            // Hash just the actual string or bytes content for consistency with heap Str/Bytes\n            // hence we don't include the discriminant\n            Self::InternString(string_id) => {\n                let mut hasher = DefaultHasher::new();\n                interns.get_str(*string_id).hash(&mut hasher);\n                return Ok(Some(hasher.finish()));\n            }\n            Self::InternBytes(bytes_id) => {\n                let mut hasher = DefaultHasher::new();\n                interns.get_bytes(*bytes_id).hash(&mut hasher);\n                return Ok(Some(hasher.finish()));\n            }\n            // Hash BigInt consistently with LongInt (using sign and bytes for large values)\n            Self::InternLongInt(long_int_id) => {\n                let bi = interns.get_long_int(*long_int_id);\n                let mut hasher = DefaultHasher::new();\n                let (sign, bytes) = bi.to_bytes_le();\n                sign.hash(&mut hasher);\n                bytes.hash(&mut hasher);\n                return Ok(Some(hasher.finish()));\n            }\n            // For heap-allocated values (includes Range and Exception), compute hash lazily and cache it\n            Self::Ref(id) => return heap.get_or_compute_hash(*id, interns),\n            _ => {}\n        }\n\n        let mut hasher = DefaultHasher::new();\n        // hash based on discriminant to avoid collisions with different types\n        discriminant(self).hash(&mut hasher);\n        match self {\n            // Immediate values can be hashed directly\n            Self::Undefined | Self::Ellipsis | Self::None => {}\n            Self::Bool(b) => b.hash(&mut hasher),\n            Self::Int(i) => i.hash(&mut hasher),\n            // Hash the bit representation of float for consistency\n            Self::Float(f) => f.to_bits().hash(&mut hasher),\n            Self::Builtin(b) => b.hash(&mut hasher),\n            Self::ModuleFunction(mf) => mf.hash(&mut hasher),\n            // Hash functions based on function ID\n            Self::DefFunction(f_id) => f_id.hash(&mut hasher),\n            Self::ExtFunction(name_id) => name_id.hash(&mut hasher),\n            // Markers are hashable based on their discriminant (already included above)\n            Self::Marker(m) => m.hash(&mut hasher),\n            // Properties are hashable based on their OS function discriminant\n            Self::Property(p) => p.hash(&mut hasher),\n            // ExternalFutures are hashable based on their call ID\n            Self::ExternalFuture(call_id) => call_id.raw().hash(&mut hasher),\n            Self::InternString(_) | Self::InternBytes(_) | Self::InternLongInt(_) | Self::Ref(_) => {\n                unreachable!(\"covered above\")\n            }\n            #[cfg(feature = \"ref-count-panic\")]\n            Self::Dereferenced => panic!(\"Cannot access Dereferenced object\"),\n        }\n        Ok(Some(hasher.finish()))\n    }\n\n    /// TODO this doesn't have many tests!!! also doesn't cover bytes\n    /// Checks if `item` is contained in `self` (the container).\n    ///\n    /// Implements Python's `in` operator for various container types:\n    /// - List/Tuple: linear search with equality\n    /// - Dict: key lookup\n    /// - Set/FrozenSet: element lookup\n    /// - Str: substring search\n    pub fn py_contains(&self, item: &Self, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<bool> {\n        match self {\n            Self::Ref(heap_id) => Heap::with_entry_mut(vm, *heap_id, |vm, data| match data {\n                HeapDataMut::List(list) => {\n                    for el in list.as_slice() {\n                        if item.py_eq(el, vm)? {\n                            return Ok(true);\n                        }\n                    }\n                    Ok(false)\n                }\n                HeapDataMut::Tuple(tuple) => {\n                    for el in tuple.as_slice() {\n                        if item.py_eq(el, vm)? {\n                            return Ok(true);\n                        }\n                    }\n                    Ok(false)\n                }\n                HeapDataMut::Dict(dict) => dict.get(item, vm).map(|m| m.is_some()),\n                HeapDataMut::DictKeysView(view) => Heap::with_entry_mut(vm, view.dict_id(), |vm, dict_data| {\n                    let HeapDataMut::Dict(dict) = dict_data else {\n                        panic!(\"dict_keys view must reference a dict\");\n                    };\n                    dict.get(item, vm).map(|m| m.is_some())\n                }),\n                HeapDataMut::DictItemsView(view) => {\n                    let Some((key, value)) = cloned_items_view_candidate(item, vm) else {\n                        return Ok(false);\n                    };\n                    let mut key_guard = HeapGuard::new(key, vm);\n                    let (key, vm) = key_guard.as_parts_mut();\n                    let mut value_guard = HeapGuard::new(value, vm);\n                    let (value, vm) = value_guard.as_parts_mut();\n                    Heap::with_entry_mut(vm, view.dict_id(), |vm, dict_data| {\n                        let HeapDataMut::Dict(dict) = dict_data else {\n                            panic!(\"dict_items view must reference a dict\");\n                        };\n                        match dict.get(key, vm) {\n                            Ok(Some(existing_value)) => value.py_eq(existing_value, vm).map_err(RunError::from),\n                            Ok(None) => Ok(false),\n                            Err(e) => Err(e),\n                        }\n                    })\n                }\n                HeapDataMut::DictValuesView(view) => Heap::with_entry_mut(vm, view.dict_id(), |vm, dict_data| {\n                    let HeapDataMut::Dict(dict) = dict_data else {\n                        panic!(\"dict_values view must reference a dict\");\n                    };\n                    for (_, value) in dict.iter() {\n                        if item.py_eq(value, vm)? {\n                            return Ok(true);\n                        }\n                    }\n                    Ok(false)\n                }),\n                HeapDataMut::Set(set) => set.contains(item, vm),\n                HeapDataMut::FrozenSet(fset) => fset.contains(item, vm),\n                HeapDataMut::Str(s) => str_contains(s.as_str(), item, vm.heap, vm.interns),\n                HeapDataMut::Range(range) => {\n                    // Range containment is O(1) - check bounds and step alignment\n                    let n = match item {\n                        Self::Int(i) => *i,\n                        Self::Bool(b) => i64::from(*b),\n                        Self::Float(f) => {\n                            // Floats are contained if they equal an integer in the range\n                            // e.g., 3.0 in range(5) is True, but 3.5 in range(5) is False\n                            if f.fract() != 0.0 {\n                                return Ok(false);\n                            }\n                            // Check if float is within i64 range and convert safely\n                            // f64 can represent integers up to 2^53 exactly\n                            let int_val = f.trunc();\n                            if int_val < i64::MIN as f64 || int_val > i64::MAX as f64 {\n                                return Ok(false);\n                            }\n                            // Safe conversion: we've verified it's a whole number in i64 range\n                            #[expect(clippy::cast_possible_truncation)]\n                            let n = int_val as i64;\n                            n\n                        }\n                        _ => return Ok(false),\n                    };\n                    Ok(range.contains(n))\n                }\n                other => {\n                    let type_name = other.py_type(vm.heap);\n                    Err(ExcType::type_error(format!(\n                        \"argument of type '{type_name}' is not iterable\"\n                    )))\n                }\n            }),\n            Self::InternString(string_id) => {\n                let container_str = vm.interns.get_str(*string_id);\n                str_contains(container_str, item, vm.heap, vm.interns)\n            }\n            _ => {\n                let type_name = self.py_type(vm.heap);\n                Err(ExcType::type_error(format!(\n                    \"argument of type '{type_name}' is not iterable\"\n                )))\n            }\n        }\n    }\n\n    /// Gets an attribute from this value.\n    ///\n    /// Dispatches to `py_getattr` on the underlying types where appropriate.\n    /// Accepts `EitherStr` to support both interned and heap-allocated attribute names.\n    ///\n    /// Returns `AttributeError` for other types or unknown attributes.\n    pub fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'_, '_, impl ResourceTracker>) -> RunResult<CallResult> {\n        match self {\n            Self::Ref(heap_id) => {\n                // Use with_entry_mut to get access to both data and heap without borrow conflicts.\n                // This allows py_getattr to allocate (for computed attributes) while we hold the data.\n                let opt_result = Heap::with_entry_mut(vm, *heap_id, |vm, data| data.py_getattr(attr, vm))?;\n                if let Some(call_result) = opt_result {\n                    return Ok(call_result);\n                }\n            }\n            Self::Builtin(Builtins::Type(t)) => {\n                // Handle type object attributes like __name__\n                let is_dunder_name = attr.static_string().map_or_else(\n                    || attr.as_str(vm.interns) == \"__name__\",\n                    |ss| ss == StaticStrings::DunderName,\n                );\n                if is_dunder_name {\n                    let name_str = t.to_string();\n                    let str_id = vm.heap.allocate(HeapData::Str(Str::from(name_str)))?;\n                    return Ok(CallResult::Value(Self::Ref(str_id)));\n                }\n            }\n            _ => {}\n        }\n        let type_name = self.py_type(vm.heap);\n        Err(ExcType::attribute_error(type_name, attr.as_str(vm.interns)))\n    }\n\n    /// Sets an attribute on this value.\n    ///\n    /// Currently only Dataclass objects support attribute setting.\n    /// Returns AttributeError for other types.\n    ///\n    /// Takes ownership of `value` and drops it on error.\n    /// On success, drops the old attribute value if one existed.\n    pub fn py_set_attr(\n        &self,\n        name_id: StringId,\n        value: Self,\n        vm: &mut VM<'_, '_, impl ResourceTracker>,\n    ) -> RunResult<()> {\n        let attr_name = vm.interns.get_str(name_id);\n\n        if let Self::Ref(heap_id) = self {\n            let heap_id = *heap_id;\n            let is_dataclass = matches!(vm.heap.get(heap_id), HeapData::Dataclass(_));\n\n            if is_dataclass {\n                let name_value = Self::InternString(name_id);\n                Heap::with_entry_mut(vm, heap_id, |vm, data| {\n                    if let HeapDataMut::Dataclass(dc) = data {\n                        match dc.set_attr(name_value, value, vm) {\n                            Ok(old_value) => {\n                                if let Some(old) = old_value {\n                                    old.drop_with_heap(vm.heap);\n                                }\n                                Ok(())\n                            }\n                            Err(e) => Err(e),\n                        }\n                    } else {\n                        unreachable!(\"type changed during borrow\")\n                    }\n                })\n            } else {\n                let type_name = vm.heap.get(heap_id).py_type(vm.heap);\n                value.drop_with_heap(vm.heap);\n                Err(ExcType::attribute_error_no_setattr(type_name, attr_name))\n            }\n        } else {\n            let type_name = self.py_type(vm.heap);\n            value.drop_with_heap(vm.heap);\n            Err(ExcType::attribute_error_no_setattr(type_name, attr_name))\n        }\n    }\n\n    /// Extracts an integer value from the Value.\n    ///\n    /// Accepts `Int` and `LongInt` (if it fits in i64). Returns a `TypeError` for other types\n    /// and an `OverflowError` if the `LongInt` value is too large.\n    ///\n    /// Note: The LongInt-to-i64 conversion path is defensive code. In normal execution,\n    /// heap-allocated `LongInt` values always exceed i64 range because `LongInt::into_value()`\n    /// automatically demotes i64-fitting values to `Value::Int`. However, this path could be\n    /// reached via deserialization of crafted snapshot data.\n    pub fn as_int(&self, heap: &Heap<impl ResourceTracker>) -> RunResult<i64> {\n        match self {\n            Self::Int(i) => Ok(*i),\n            Self::Ref(heap_id) => {\n                if let HeapData::LongInt(li) = heap.get(*heap_id) {\n                    li.to_i64().ok_or_else(ExcType::overflow_shift_count)\n                } else {\n                    let msg = format!(\"'{}' object cannot be interpreted as an integer\", self.py_type(heap));\n                    Err(SimpleException::new_msg(ExcType::TypeError, msg).into())\n                }\n            }\n            _ => {\n                let msg = format!(\"'{}' object cannot be interpreted as an integer\", self.py_type(heap));\n                Err(SimpleException::new_msg(ExcType::TypeError, msg).into())\n            }\n        }\n    }\n\n    /// Extracts an index value for sequence operations.\n    ///\n    /// Accepts `Int`, `Bool` (True=1, False=0), and `LongInt` (if it fits in i64).\n    /// Returns a `TypeError` for other types with the container type name included.\n    /// Returns an `IndexError` if the `LongInt` value is too large to use as an index.\n    ///\n    /// Note: The LongInt-to-i64 conversion path is defensive code. In normal execution,\n    /// heap-allocated `LongInt` values always exceed i64 range because `LongInt::into_value()`\n    /// automatically demotes i64-fitting values to `Value::Int`. However, this path could be\n    /// reached via deserialization of crafted snapshot data.\n    pub fn as_index(&self, heap: &Heap<impl ResourceTracker>, container_type: Type) -> RunResult<i64> {\n        match self {\n            Self::Int(i) => Ok(*i),\n            Self::Bool(b) => Ok(i64::from(*b)),\n            Self::Ref(heap_id) => {\n                if let HeapData::LongInt(li) = heap.get(*heap_id) {\n                    li.to_i64().ok_or_else(ExcType::index_error_int_too_large)\n                } else {\n                    Err(ExcType::type_error_indices(container_type, self.py_type(heap)))\n                }\n            }\n            _ => Err(ExcType::type_error_indices(container_type, self.py_type(heap))),\n        }\n    }\n\n    /// Performs a binary bitwise operation on two values.\n    ///\n    /// Python only supports bitwise operations on integers (and bools, which coerce to int).\n    /// Returns a `TypeError` if either operand is not an integer, bool, or LongInt.\n    ///\n    /// For shift operations:\n    /// - Negative shift counts raise `ValueError`\n    /// - Left shifts may produce LongInt results for large shifts\n    /// - Right shifts with large counts return 0 (or -1 for negative numbers)\n    pub fn py_bitwise(\n        &self,\n        other: &Self,\n        op: BitwiseOp,\n        heap: &mut Heap<impl ResourceTracker>,\n    ) -> Result<Self, RunError> {\n        // Capture types for error messages\n        let lhs_type = self.py_type(heap);\n        let rhs_type = other.py_type(heap);\n\n        // Extract BigInt from all numeric types\n        let lhs_bigint = extract_bigint(self, heap);\n        let rhs_bigint = extract_bigint(other, heap);\n\n        if let (Some(l), Some(r)) = (lhs_bigint, rhs_bigint) {\n            let result = match op {\n                BitwiseOp::And => l & r,\n                BitwiseOp::Or => l | r,\n                BitwiseOp::Xor => l ^ r,\n                BitwiseOp::LShift => {\n                    // Get shift amount as i64 for validation\n                    let shift_amount = r.to_i64();\n                    if let Some(shift) = shift_amount {\n                        if shift < 0 {\n                            return Err(ExcType::value_error_negative_shift_count());\n                        }\n                        // Python allows arbitrarily large left shifts - use BigInt's shift\n                        // Safety: shift >= 0 is guaranteed by the check above\n                        #[expect(clippy::cast_sign_loss)]\n                        let shift_u64 = shift as u64;\n                        // Check size before computing to prevent DoS\n                        check_lshift_size(l.bits(), shift_u64, heap.tracker())?;\n                        l << shift_u64\n                    } else if r.sign() == num_bigint::Sign::Minus {\n                        return Err(ExcType::value_error_negative_shift_count());\n                    } else {\n                        // Shift amount too large to fit in i64 - this would be astronomically large\n                        return Err(ExcType::overflow_shift_count());\n                    }\n                }\n                BitwiseOp::RShift => {\n                    // Get shift amount as i64 for validation\n                    let shift_amount = r.to_i64();\n                    if let Some(shift) = shift_amount {\n                        if shift < 0 {\n                            return Err(ExcType::value_error_negative_shift_count());\n                        }\n                        // Safety: shift >= 0 is guaranteed by the check above\n                        #[expect(clippy::cast_sign_loss)]\n                        let shift_u64 = shift as u64;\n                        l >> shift_u64\n                    } else if r.sign() == num_bigint::Sign::Minus {\n                        return Err(ExcType::value_error_negative_shift_count());\n                    } else {\n                        // Shift amount too large - result is 0 or -1 depending on sign\n                        if l.sign() == num_bigint::Sign::Minus {\n                            BigInt::from(-1)\n                        } else {\n                            BigInt::from(0)\n                        }\n                    }\n                }\n            };\n            // Convert result back to Value, demoting to i64 if it fits\n            LongInt::new(result).into_value(heap).map_err(Into::into)\n        } else {\n            Err(ExcType::binary_type_error(op.as_str(), lhs_type, rhs_type))\n        }\n    }\n\n    /// Clones an value with proper heap reference counting.\n    ///\n    /// For immediate values (Int, Bool, None, etc.), this performs a simple copy.\n    /// For heap-allocated values (Ref variant), this increments the reference count\n    /// and returns a new reference to the same heap value.\n    ///\n    /// Takes `ContainsHeap` to allow directly passing the `VM` in many contexts. Where\n    /// borrow checking creates conflicts, it may be preferred to pass `&Heap` directly\n    /// (e.g. as `vm.heap` / `self.heap` etc.).\n    ///\n    /// # Important\n    /// This method MUST be used instead of the derived `Clone` implementation to ensure\n    /// proper reference counting. Using `.clone()` directly will bypass reference counting\n    /// and cause memory leaks or double-frees.\n    #[must_use]\n    pub fn clone_with_heap(&self, heap: &impl ContainsHeap) -> Self {\n        match self {\n            Self::Ref(id) => {\n                heap.heap().inc_ref(*id);\n                Self::Ref(*id)\n            }\n            // Immediate values can be copied without heap interaction\n            other => other.clone_immediate(),\n        }\n    }\n\n    /// Drops an value, decrementing its heap reference count if applicable.\n    ///\n    /// For immediate values, this is a no-op. For heap-allocated values (Ref variant),\n    /// this decrements the reference count and frees the value (and any children) when\n    /// the count reaches zero. For Closure variants, this decrements ref counts on all\n    /// captured cells.\n    ///\n    /// Takes `ContainsHeap` to allow directly passing the `VM` in many contexts. Where\n    /// borrow checking creates conflicts, it may be preferred to pass `&mut Heap` directly\n    /// (e.g. as `vm.heap` / `self.heap` etc.).\n    ///\n    /// # Important\n    /// This method MUST be called before overwriting a namespace slot or discarding\n    /// a value to prevent memory leaks.\n    #[cfg(not(feature = \"ref-count-panic\"))]\n    #[inline]\n    pub fn drop_with_heap(self, heap: &mut impl ContainsHeap) {\n        if let Self::Ref(id) = self {\n            heap.heap_mut().dec_ref(id);\n        }\n    }\n    /// With `ref-count-panic` enabled, `Ref` variants are replaced with `Dereferenced` and\n    /// the original is forgotten to prevent the Drop impl from panicking. Non-Ref variants\n    /// are left unchanged since they don't trigger the Drop panic.\n    #[cfg(feature = \"ref-count-panic\")]\n    pub fn drop_with_heap(mut self, heap: &mut impl ContainsHeap) {\n        let old = std::mem::replace(&mut self, Self::Dereferenced);\n        if let Self::Ref(id) = &old {\n            heap.heap_mut().dec_ref(*id);\n            std::mem::forget(old);\n        }\n    }\n\n    /// Internal helper for copying immediate values without heap interaction.\n    ///\n    /// This method should only be called by `clone_with_heap()` for immediate values.\n    /// Attempting to clone a Ref variant will panic.\n    pub fn clone_immediate(&self) -> Self {\n        match self {\n            Self::Undefined => Self::Undefined,\n            Self::Ellipsis => Self::Ellipsis,\n            Self::None => Self::None,\n            Self::Bool(b) => Self::Bool(*b),\n            Self::Int(v) => Self::Int(*v),\n            Self::Float(v) => Self::Float(*v),\n            Self::Builtin(b) => Self::Builtin(*b),\n            Self::ModuleFunction(mf) => Self::ModuleFunction(*mf),\n            Self::DefFunction(f) => Self::DefFunction(*f),\n            Self::ExtFunction(f) => Self::ExtFunction(*f),\n            Self::InternString(s) => Self::InternString(*s),\n            Self::InternBytes(b) => Self::InternBytes(*b),\n            Self::InternLongInt(bi) => Self::InternLongInt(*bi),\n            Self::Marker(m) => Self::Marker(*m),\n            Self::Property(p) => Self::Property(*p),\n            Self::ExternalFuture(call_id) => Self::ExternalFuture(*call_id),\n            Self::Ref(_) => panic!(\"Ref clones must go through clone_with_heap to maintain refcounts\"),\n            #[cfg(feature = \"ref-count-panic\")]\n            Self::Dereferenced => panic!(\"Cannot copy Dereferenced object\"),\n        }\n    }\n\n    /// Mark as Dereferenced to prevent Drop panic\n    ///\n    /// This should be called from `py_dec_ref_ids` methods only\n    #[cfg(feature = \"ref-count-panic\")]\n    pub fn dec_ref_forget(&mut self) {\n        let old = std::mem::replace(self, Self::Dereferenced);\n        std::mem::forget(old);\n    }\n\n    /// Pushes any contained `HeapId` onto the stack for reference counting.\n    ///\n    /// For `Value::Ref` variants, pushes the heap ID so the referenced object's\n    /// refcount can be decremented. When `ref-count-panic` is enabled, also marks\n    /// this value as `Dereferenced` to prevent Drop panics.\n    pub fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {\n        if let Self::Ref(id) = self {\n            stack.push(*id);\n            #[cfg(feature = \"ref-count-panic\")]\n            self.dec_ref_forget();\n        }\n    }\n\n    /// Converts the value into a keyword string representation if possible.\n    ///\n    /// Returns `Some(KeywordStr)` for `InternString` values or heap `str`\n    /// objects, otherwise returns `None`.\n    pub fn as_either_str(&self, heap: &Heap<impl ResourceTracker>) -> Option<EitherStr> {\n        match self {\n            Self::InternString(id) => Some(EitherStr::Interned(*id)),\n            Self::Ref(heap_id) => match heap.get(*heap_id) {\n                HeapData::Str(s) => Some(EitherStr::Heap(s.as_str().to_owned())),\n                _ => None,\n            },\n            _ => None,\n        }\n    }\n\n    /// check if the value is a string.\n    pub fn is_str(&self, heap: &Heap<impl ResourceTracker>) -> bool {\n        match self {\n            Self::InternString(_) => true,\n            Self::Ref(heap_id) => matches!(heap.get(*heap_id), HeapData::Str(_)),\n            _ => false,\n        }\n    }\n}\n\n/// Interned or heap-owned string identifier.\n///\n/// Used when a string value can come from either the intern table (for known\n/// static strings and keywords) or from a heap-allocated Python string object.\n#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) enum EitherStr {\n    /// Interned string identifier (cheap comparisons and no allocation).\n    Interned(StringId),\n    /// Heap-owned string extracted from a `str` object.\n    Heap(String),\n}\n\nimpl From<StringId> for EitherStr {\n    fn from(id: StringId) -> Self {\n        Self::Interned(id)\n    }\n}\n\nimpl From<StaticStrings> for EitherStr {\n    fn from(s: StaticStrings) -> Self {\n        Self::Interned(s.into())\n    }\n}\n\n/// Convert String to EitherStr: use Interned for known static strings,\n/// otherwise use Heap for user-defined field names.\nimpl From<String> for EitherStr {\n    fn from(s: String) -> Self {\n        match StaticStrings::from_str(&s) {\n            Ok(s) => s.into(),\n            Err(_) => Self::Heap(s),\n        }\n    }\n}\n\nimpl EitherStr {\n    /// Returns the keyword as a str slice for error messages or comparisons.\n    pub fn as_str<'a>(&'a self, interns: &'a Interns) -> &'a str {\n        match self {\n            Self::Interned(id) => interns.get_str(*id),\n            Self::Heap(s) => s.as_str(),\n        }\n    }\n\n    /// Checks whether this keyword matches the given interned identifier.\n    pub fn matches(&self, target: StringId, interns: &Interns) -> bool {\n        match self {\n            Self::Interned(id) => *id == target,\n            Self::Heap(s) => s == interns.get_str(target),\n        }\n    }\n\n    /// Returns the `StringId` if this is an interned attribute.\n    #[inline]\n    pub fn string_id(&self) -> Option<StringId> {\n        match self {\n            Self::Interned(id) => Some(*id),\n            Self::Heap(_) => None,\n        }\n    }\n\n    /// Returns the `StaticStrings` if this is an interned attribute from `StaticStrings`s.\n    #[inline]\n    pub fn static_string(&self) -> Option<StaticStrings> {\n        match self {\n            Self::Interned(id) => StaticStrings::from_string_id(*id),\n            Self::Heap(_) => None,\n        }\n    }\n\n    /// Converts this `EitherStr` into an owned `String`.\n    ///\n    /// For interned strings, looks up and clones the string content.\n    /// For heap strings, returns the owned string directly.\n    pub fn into_string(self, interns: &Interns) -> String {\n        match self {\n            Self::Interned(id) => interns.get_str(id).to_owned(),\n            Self::Heap(s) => s,\n        }\n    }\n\n    pub fn py_estimate_size(&self) -> usize {\n        match self {\n            Self::Interned(_) => 0,\n            Self::Heap(s) => s.capacity(),\n        }\n    }\n}\n\n/// Bitwise operation type for `py_bitwise`.\n#[derive(Debug, Clone, Copy)]\npub enum BitwiseOp {\n    And,\n    Or,\n    Xor,\n    LShift,\n    RShift,\n}\n\nimpl BitwiseOp {\n    /// Returns the operator symbol for error messages.\n    pub fn as_str(self) -> &'static str {\n        match self {\n            Self::And => \"&\",\n            Self::Or => \"|\",\n            Self::Xor => \"^\",\n            Self::LShift => \"<<\",\n            Self::RShift => \">>\",\n        }\n    }\n}\n\n/// Marker values for special objects that exist but have minimal functionality.\n///\n/// These are used for:\n/// - System objects like `sys.stdout` and `sys.stderr` that need to exist but don't\n///   provide functionality in the sandboxed environment\n/// - Typing constructs from the `typing` module that are imported for type hints but\n///   don't need runtime functionality\n///\n/// Wraps a `StaticStrings` variant to leverage its string conversion capabilities.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub(crate) struct Marker(pub StaticStrings);\n\nimpl Marker {\n    /// Returns the Python type of this marker.\n    ///\n    /// System markers (stdout, stderr) are `TextIOWrapper`.\n    /// `typing.Union` has type `type` (matching CPython).\n    /// Other typing markers (Any, Optional, etc.) are `_SpecialForm`.\n    pub(crate) fn py_type(self) -> Type {\n        match self.0 {\n            StaticStrings::Stdout | StaticStrings::Stderr => Type::TextIOWrapper,\n            StaticStrings::UnionType => Type::Type,\n            _ => Type::SpecialForm,\n        }\n    }\n\n    /// Writes the Python repr for this marker.\n    ///\n    /// System markers have special repr formats (\"<stdout>\", \"<stderr>\").\n    /// `typing.Union` uses `<class 'typing.Union'>` format (matching CPython).\n    /// Other typing markers are prefixed with \"typing.\" (e.g., \"typing.Any\").\n    fn py_repr_fmt(self, f: &mut impl Write) -> fmt::Result {\n        let s: &'static str = self.0.into();\n        match self.0 {\n            StaticStrings::Stdout => f.write_str(\"<stdout>\")?,\n            StaticStrings::Stderr => f.write_str(\"<stderr>\")?,\n            StaticStrings::UnionType => f.write_str(\"<class 'typing.Union'>\")?,\n            _ => write!(f, \"typing.{s}\")?,\n        }\n        Ok(())\n    }\n}\n\n/// High-bit tag reserved for literal singletons (None, Ellipsis, booleans).\nconst SINGLETON_ID_TAG: usize = 1usize << (usize::BITS - 1);\n/// High-bit tag reserved for interned string `id()` values.\nconst INTERN_STR_ID_TAG: usize = 1usize << (usize::BITS - 2);\n/// High-bit tag reserved for interned bytes `id()` values to avoid colliding with any other space.\nconst INTERN_BYTES_ID_TAG: usize = 1usize << (usize::BITS - 3);\n/// High-bit tag reserved for heap-backed `HeapId`s.\nconst HEAP_ID_TAG: usize = 1usize << (usize::BITS - 4);\n\n/// Mask that keeps pointer-derived bits below the bytes tag bit.\nconst INTERN_BYTES_ID_MASK: usize = INTERN_BYTES_ID_TAG - 1;\n/// Mask that keeps pointer-derived bits below the string tag bit.\nconst INTERN_STR_ID_MASK: usize = INTERN_STR_ID_TAG - 1;\n/// Mask that keeps per-singleton offsets below the singleton tag bit.\nconst SINGLETON_ID_MASK: usize = SINGLETON_ID_TAG - 1;\n/// Mask that keeps heap value IDs below the heap tag bit.\nconst HEAP_ID_MASK: usize = HEAP_ID_TAG - 1;\n\n/// High-bit tag for Int value-based IDs (no heap allocation needed).\nconst INT_ID_TAG: usize = 1usize << (usize::BITS - 5);\n/// High-bit tag for Float value-based IDs.\nconst FLOAT_ID_TAG: usize = 1usize << (usize::BITS - 6);\n/// High-bit tag for Callable value-based IDs.\nconst BUILTIN_ID_TAG: usize = 1usize << (usize::BITS - 7);\n/// High-bit tag for Function value-based IDs.\nconst FUNCTION_ID_TAG: usize = 1usize << (usize::BITS - 8);\n/// High-bit tag for External Function value-based IDs.\nconst EXTFUNCTION_ID_TAG: usize = 1usize << (usize::BITS - 9);\n/// High-bit tag for Marker value-based IDs (stdout, stderr, etc.).\nconst MARKER_ID_TAG: usize = 1usize << (usize::BITS - 10);\n/// High-bit tag for ExternalFuture value-based IDs.\nconst EXTERNAL_FUTURE_ID_TAG: usize = 1usize << (usize::BITS - 11);\n/// High-bit tag for ModuleFunction value-based IDs.\nconst MODULE_FUNCTION_ID_TAG: usize = 1usize << (usize::BITS - 12);\n/// High-bit tag for interned LongInt `id()` values.\nconst INTERN_LONG_INT_ID_TAG: usize = 1usize << (usize::BITS - 13);\n/// High-bit tag for Property value-based IDs.\nconst PROPERTY_ID_TAG: usize = 1usize << (usize::BITS - 14);\n\n/// Masks for value-based ID tags (keep bits below the tag bit).\nconst INT_ID_MASK: usize = INT_ID_TAG - 1;\nconst FLOAT_ID_MASK: usize = FLOAT_ID_TAG - 1;\nconst BUILTIN_ID_MASK: usize = BUILTIN_ID_TAG - 1;\nconst FUNCTION_ID_MASK: usize = FUNCTION_ID_TAG - 1;\nconst EXTFUNCTION_ID_MASK: usize = EXTFUNCTION_ID_TAG - 1;\nconst MARKER_ID_MASK: usize = MARKER_ID_TAG - 1;\nconst EXTERNAL_FUTURE_ID_MASK: usize = EXTERNAL_FUTURE_ID_TAG - 1;\nconst MODULE_FUNCTION_ID_MASK: usize = MODULE_FUNCTION_ID_TAG - 1;\nconst INTERN_LONG_INT_ID_MASK: usize = INTERN_LONG_INT_ID_TAG - 1;\nconst PROPERTY_ID_MASK: usize = PROPERTY_ID_TAG - 1;\n\n/// Enumerates singleton literal slots so we can issue stable `id()` values without heap allocation.\n#[repr(usize)]\n#[derive(Copy, Clone)]\nenum SingletonSlot {\n    Undefined = 0,\n    Ellipsis = 1,\n    None = 2,\n    False = 3,\n    True = 4,\n}\n\n/// Returns the fully tagged `id()` value for the requested singleton literal.\n#[inline]\nconst fn singleton_id(slot: SingletonSlot) -> usize {\n    SINGLETON_ID_TAG | ((slot as usize) & SINGLETON_ID_MASK)\n}\n\n/// Computes Python-style floor division and modulo.\n///\n/// Python's division rounds toward negative infinity (floor division),\n/// and the remainder has the same sign as the divisor.\n/// This differs from Rust's truncating division.\n///\n/// Returns `None` on overflow (i64::MIN / -1 doesn't fit in i64).\npub(crate) fn floor_divmod(a: i64, b: i64) -> Option<(i64, i64)> {\n    let quot = a.checked_div(b)?;\n    let rem = a.checked_rem(b)?;\n\n    if rem != 0 && (rem < 0) != (b < 0) {\n        Some((quot - 1, rem + b))\n    } else {\n        Some((quot, rem))\n    }\n}\n\n/// Converts a heap `HeapId` into its tagged `id()` value, ensuring it never collides with other spaces.\n#[inline]\npub fn heap_tagged_id(heap_id: HeapId) -> usize {\n    HEAP_ID_TAG | (heap_id.index() & HEAP_ID_MASK)\n}\n\n/// Computes a deterministic ID for an i64 integer value.\n/// Uses the value's hash combined with a type tag to ensure uniqueness across types.\n#[inline]\nfn int_value_id(value: i64) -> usize {\n    let mut hasher = DefaultHasher::new();\n    value.hash(&mut hasher);\n    let hash_u64 = hasher.finish();\n    // Mask to usize range before conversion to handle 32-bit platforms\n    let masked = hash_u64 & (usize::MAX as u64);\n    let hash_usize = usize::try_from(masked).expect(\"masked value fits in usize\");\n    INT_ID_TAG | (hash_usize & INT_ID_MASK)\n}\n\n/// Computes a deterministic ID for an f64 float value.\n/// Uses the bit representation's hash for consistency (handles NaN, infinities, etc.).\n#[inline]\nfn float_value_id(value: f64) -> usize {\n    let mut hasher = DefaultHasher::new();\n    value.to_bits().hash(&mut hasher);\n    let hash_u64 = hasher.finish();\n    // Mask to usize range before conversion to handle 32-bit platforms\n    let masked = hash_u64 & (usize::MAX as u64);\n    let hash_usize = usize::try_from(masked).expect(\"masked value fits in usize\");\n    FLOAT_ID_TAG | (hash_usize & FLOAT_ID_MASK)\n}\n\n/// Computes a deterministic ID for a builtin based on its discriminant.\n#[inline]\nfn builtin_value_id(b: Builtins) -> usize {\n    let mut hasher = DefaultHasher::new();\n    b.hash(&mut hasher);\n    let hash_u64 = hasher.finish();\n    // wrapping here is fine\n    #[expect(clippy::cast_possible_truncation)]\n    let hash_usize = hash_u64 as usize;\n    BUILTIN_ID_TAG | (hash_usize & BUILTIN_ID_MASK)\n}\n\n/// Computes a deterministic ID for a function based on its id.\n#[inline]\nfn function_value_id(f_id: FunctionId) -> usize {\n    FUNCTION_ID_TAG | (f_id.index() & FUNCTION_ID_MASK)\n}\n\n/// Computes a deterministic ID for an external function based on its interned name.\n#[inline]\nfn ext_function_value_id(name_id: StringId) -> usize {\n    EXTFUNCTION_ID_TAG | (name_id.index() & EXTFUNCTION_ID_MASK)\n}\n\n/// Computes a deterministic ID for a marker value based on its discriminant.\n#[inline]\nfn marker_value_id(m: Marker) -> usize {\n    MARKER_ID_TAG | ((m.0 as usize) & MARKER_ID_MASK)\n}\n\n/// Computes a deterministic ID for a property value based on its discriminant.\n#[inline]\nfn property_value_id(p: Property) -> usize {\n    let discriminant = match p {\n        Property::Os(os_fn) => os_fn as usize,\n    };\n    PROPERTY_ID_TAG | (discriminant & PROPERTY_ID_MASK)\n}\n\n/// Computes a deterministic ID for an external future based on its call ID.\n#[inline]\nfn external_future_value_id(call_id: CallId) -> usize {\n    EXTERNAL_FUTURE_ID_TAG | ((call_id.raw() as usize) & EXTERNAL_FUTURE_ID_MASK)\n}\n\n/// Computes a deterministic ID for a module function based on its discriminant.\n#[inline]\nfn module_function_value_id(mf: ModuleFunctions) -> usize {\n    let mut hasher = DefaultHasher::new();\n    mf.hash(&mut hasher);\n    let hash_u64 = hasher.finish();\n    // wrapping here is fine\n    #[expect(clippy::cast_possible_truncation)]\n    let hash_usize = hash_u64 as usize;\n    MODULE_FUNCTION_ID_TAG | (hash_usize & MODULE_FUNCTION_ID_MASK)\n}\n\n/// Converts an i64 repeat count to usize, handling negative values and overflow.\n///\n/// Returns 0 for negative values (Python treats negative repeat counts as 0).\n/// Returns `OverflowError` if the value exceeds `usize::MAX`.\n#[inline]\nfn i64_to_repeat_count(n: i64) -> RunResult<usize> {\n    if n <= 0 {\n        Ok(0)\n    } else {\n        usize::try_from(n).map_err(|_| ExcType::overflow_repeat_count().into())\n    }\n}\n\n/// Converts a LongInt repeat count to usize, handling negative values and overflow.\n///\n/// Returns 0 for negative values (Python treats negative repeat counts as 0).\n/// Returns `OverflowError` if the value exceeds `usize::MAX`.\n#[inline]\nfn longint_to_repeat_count(li: &LongInt) -> RunResult<usize> {\n    if li.is_negative() {\n        Ok(0)\n    } else if let Some(count) = li.to_usize() {\n        Ok(count)\n    } else {\n        Err(ExcType::overflow_repeat_count().into())\n    }\n}\n\n/// Extracts a BigInt from a Value for bitwise operations.\n///\n/// Returns `Some(BigInt)` for Int, Bool, and LongInt values.\n/// Returns `None` for other types (Float, Str, etc.).\nfn extract_bigint(value: &Value, heap: &Heap<impl ResourceTracker>) -> Option<BigInt> {\n    match value {\n        Value::Int(i) => Some(BigInt::from(*i)),\n        Value::Bool(b) => Some(BigInt::from(i64::from(*b))),\n        Value::Ref(id) => {\n            if let HeapData::LongInt(li) = heap.get(*id) {\n                Some(li.inner().clone())\n            } else {\n                None\n            }\n        }\n        _ => None,\n    }\n}\n\n/// Extracts and clones the `(key, value)` probe accepted by `dict_items.__contains__`.\n///\n/// CPython treats only 2-tuples as valid probes for items-view membership. Monty\n/// also accepts namedtuples of length two so tuple-like runtime values behave\n/// sensibly even though namedtuples are not modeled as a true tuple subclass.\nfn cloned_items_view_candidate(item: &Value, heap: &impl ContainsHeap) -> Option<(Value, Value)> {\n    let Value::Ref(heap_id) = item else {\n        return None;\n    };\n\n    match heap.heap().get(*heap_id) {\n        HeapData::Tuple(tuple) => {\n            let items = tuple.as_slice();\n            if items.len() == 2 {\n                Some((items[0].clone_with_heap(heap), items[1].clone_with_heap(heap)))\n            } else {\n                None\n            }\n        }\n        HeapData::NamedTuple(namedtuple) => {\n            let items = namedtuple.as_vec();\n            if items.len() == 2 {\n                Some((items[0].clone_with_heap(heap), items[1].clone_with_heap(heap)))\n            } else {\n                None\n            }\n        }\n        _ => None,\n    }\n}\n\n/// Helper for substring containment check in strings.\n///\n/// Called by `py_contains` when the container is a string.\n/// The item must also be a string (either interned or heap-allocated).\nfn str_contains(\n    container_str: &str,\n    item: &Value,\n    heap: &mut Heap<impl ResourceTracker>,\n    interns: &Interns,\n) -> RunResult<bool> {\n    match item {\n        Value::InternString(item_id) => {\n            let item_str = interns.get_str(*item_id);\n            Ok(container_str.contains(item_str))\n        }\n        Value::Ref(item_heap_id) => {\n            if let HeapData::Str(item_str) = heap.get(*item_heap_id) {\n                Ok(container_str.contains(item_str.as_str()))\n            } else {\n                Err(ExcType::type_error(\"'in <str>' requires string as left operand\"))\n            }\n        }\n        _ => Err(ExcType::type_error(\"'in <str>' requires string as left operand\")),\n    }\n}\n\n/// Computes the number of significant bits in an i64.\n///\n/// Returns 0 for 0, otherwise returns ceil(log2(|value|)) + 1 (accounting for sign).\n/// For example: 0 -> 0, 1 -> 1, 2 -> 2, 255 -> 8, 256 -> 9.\nfn i64_bits(value: i64) -> u64 {\n    if value == 0 {\n        0\n    } else {\n        // For negative numbers, use unsigned_abs to get magnitude\n        u64::from(64 - value.unsigned_abs().leading_zeros())\n    }\n}\n\n/// Computes BigInt exponentiation for exponents larger than u32::MAX.\n///\n/// Uses repeated squaring for efficiency. This is needed when the exponent\n/// doesn't fit in a u32, which is required by the `num-bigint` pow method.\nfn bigint_pow(base: BigInt, exp: u64) -> BigInt {\n    if exp == 0 {\n        return BigInt::from(1);\n    }\n    if exp == 1 {\n        return base;\n    }\n\n    // Use repeated squaring\n    let mut result = BigInt::from(1);\n    let mut b = base;\n    let mut e = exp;\n\n    while e > 0 {\n        if e & 1 == 1 {\n            result *= &b;\n        }\n        b = &b * &b;\n        e >>= 1;\n    }\n\n    result\n}\n\n#[cfg(test)]\nmod tests {\n    use num_bigint::BigInt;\n\n    use super::*;\n    use crate::resource::NoLimitTracker;\n\n    /// Creates a heap and directly allocates a LongInt with the given BigInt value.\n    ///\n    /// This bypasses `LongInt::into_value()` which would demote i64-fitting values.\n    /// Used to test defensive code paths that handle LongInt-as-index scenarios.\n    fn create_heap_with_longint(value: BigInt) -> (Heap<NoLimitTracker>, HeapId) {\n        let mut heap = Heap::new(16, NoLimitTracker);\n        let long_int = LongInt::new(value);\n        let heap_id = heap.allocate(HeapData::LongInt(long_int)).unwrap();\n        (heap, heap_id)\n    }\n\n    /// Tests that `as_index()` correctly handles a LongInt containing an i64-fitting value.\n    ///\n    /// This tests a defensive code path that's normally unreachable because\n    /// `LongInt::into_value()` demotes i64-fitting values to `Value::Int`.\n    /// However, this path could be reached via deserialization of crafted data.\n    #[test]\n    fn as_index_longint_fits_in_i64() {\n        let (mut heap, heap_id) = create_heap_with_longint(BigInt::from(42));\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert_eq!(result.unwrap(), 42);\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests that `as_index()` correctly handles a negative LongInt that fits in i64.\n    #[test]\n    fn as_index_longint_negative_fits_in_i64() {\n        let (mut heap, heap_id) = create_heap_with_longint(BigInt::from(-100));\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert_eq!(result.unwrap(), -100);\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests that `as_index()` returns IndexError for LongInt values too large for i64.\n    #[test]\n    fn as_index_longint_too_large() {\n        // 2^100 is way larger than i64::MAX\n        let big_value = BigInt::from(2).pow(100);\n        let (mut heap, heap_id) = create_heap_with_longint(big_value);\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert!(result.is_err());\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests that `as_int()` correctly handles a LongInt containing an i64-fitting value.\n    ///\n    /// Similar to `as_index`, this tests a defensive code path normally unreachable.\n    #[test]\n    fn as_int_longint_fits_in_i64() {\n        let (mut heap, heap_id) = create_heap_with_longint(BigInt::from(12345));\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_int(&heap);\n        assert_eq!(result.unwrap(), 12345);\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests that `as_int()` returns an error for LongInt values too large for i64.\n    #[test]\n    fn as_int_longint_too_large() {\n        let big_value = BigInt::from(2).pow(100);\n        let (mut heap, heap_id) = create_heap_with_longint(big_value);\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_int(&heap);\n        assert!(result.is_err());\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests boundary values: i64::MAX as a LongInt.\n    #[test]\n    fn as_index_longint_at_i64_max() {\n        let (mut heap, heap_id) = create_heap_with_longint(BigInt::from(i64::MAX));\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert_eq!(result.unwrap(), i64::MAX);\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests boundary values: i64::MIN as a LongInt.\n    #[test]\n    fn as_index_longint_at_i64_min() {\n        let (mut heap, heap_id) = create_heap_with_longint(BigInt::from(i64::MIN));\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert_eq!(result.unwrap(), i64::MIN);\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests boundary values: i64::MAX + 1 as a LongInt (should fail).\n    #[test]\n    fn as_index_longint_just_over_i64_max() {\n        let big_value = BigInt::from(i64::MAX) + BigInt::from(1);\n        let (mut heap, heap_id) = create_heap_with_longint(big_value);\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert!(result.is_err());\n        value.drop_with_heap(&mut heap);\n    }\n\n    /// Tests boundary values: i64::MIN - 1 as a LongInt (should fail).\n    #[test]\n    fn as_index_longint_just_under_i64_min() {\n        let big_value = BigInt::from(i64::MIN) - BigInt::from(1);\n        let (mut heap, heap_id) = create_heap_with_longint(big_value);\n        let value = Value::Ref(heap_id);\n\n        let result = value.as_index(&heap, Type::List);\n        assert!(result.is_err());\n        value.drop_with_heap(&mut heap);\n    }\n}\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_get_no_args.py",
    "content": "x = {}\nx.get()\n# Raise=TypeError('get expected at least 1 argument, got 0')\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_get_too_many.py",
    "content": "x = {}\nx.get(1, 2, 3)\n# Raise=TypeError('get expected at most 2 arguments, got 3')\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_items_with_args.py",
    "content": "x = {}\nx.items(1)\n# Raise=TypeError('dict.items() takes no arguments (1 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_keys_with_args.py",
    "content": "x = {}\nx.keys(1)\n# Raise=TypeError('dict.keys() takes no arguments (1 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_pop_no_args.py",
    "content": "x = {}\nx.pop()\n# Raise=TypeError('pop expected at least 1 argument, got 0')\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_pop_too_many.py",
    "content": "x = {}\nx.pop(1, 2, 3)\n# Raise=TypeError('pop expected at most 2 arguments, got 3')\n"
  },
  {
    "path": "crates/monty/test_cases/args__dict_values_with_args.py",
    "content": "x = {}\nx.values(1)\n# Raise=TypeError('dict.values() takes no arguments (1 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__id_too_many.py",
    "content": "id(1, 2)\n# Raise=TypeError('id() takes exactly one argument (2 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__len_no_args.py",
    "content": "len()\n# Raise=TypeError('len() takes exactly one argument (0 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__len_too_many.py",
    "content": "len(1, 2)\n# Raise=TypeError('len() takes exactly one argument (2 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__len_type_error_int.py",
    "content": "len(42)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"args__len_type_error_int.py\", line 1, in <module>\n    len(42)\n    ~~~~~~~\nTypeError: object of type 'int' has no len()\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/args__len_type_error_none.py",
    "content": "len(None)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"args__len_type_error_none.py\", line 1, in <module>\n    len(None)\n    ~~~~~~~~~\nTypeError: object of type 'NoneType' has no len()\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/args__list_append_no_args.py",
    "content": "x = []\nx.append()\n# Raise=TypeError('list.append() takes exactly one argument (0 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__list_append_too_many.py",
    "content": "x = []\nx.append(1, 2)\n# Raise=TypeError('list.append() takes exactly one argument (2 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/args__list_insert_too_few.py",
    "content": "x = []\nx.insert(1)\n# Raise=TypeError('insert expected 2 arguments, got 1')\n"
  },
  {
    "path": "crates/monty/test_cases/args__list_insert_too_many.py",
    "content": "x = []\nx.insert(1, 2, 3)\n# Raise=TypeError('insert expected 2 arguments, got 3')\n"
  },
  {
    "path": "crates/monty/test_cases/args__repr_no_args.py",
    "content": "repr()\n# Raise=TypeError('repr() takes exactly one argument (0 given)')\n"
  },
  {
    "path": "crates/monty/test_cases/arith__div_zero_float.py",
    "content": "1.0 / 0.0\n# Raise=ZeroDivisionError('division by zero')\n"
  },
  {
    "path": "crates/monty/test_cases/arith__div_zero_int.py",
    "content": "1 / 0\n# Raise=ZeroDivisionError('division by zero')\n"
  },
  {
    "path": "crates/monty/test_cases/arith__floordiv_zero_float.py",
    "content": "1.0 // 0.0\n# Raise=ZeroDivisionError('division by zero')\n"
  },
  {
    "path": "crates/monty/test_cases/arith__floordiv_zero_int.py",
    "content": "1 // 0\n# Raise=ZeroDivisionError('division by zero')\n"
  },
  {
    "path": "crates/monty/test_cases/arith__pow_zero_neg.py",
    "content": "0**-1\n# Raise=ZeroDivisionError('zero to a negative power')\n"
  },
  {
    "path": "crates/monty/test_cases/arith__pow_zero_neg_builtin.py",
    "content": "pow(0, -1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"arith__pow_zero_neg_builtin.py\", line 1, in <module>\n    pow(0, -1)\n    ~~~~~~~~~~\nZeroDivisionError: zero to a negative power\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/assert__expr_fail.py",
    "content": "assert 1 == 2\n# Raise=AssertionError()\n"
  },
  {
    "path": "crates/monty/test_cases/assert__fail.py",
    "content": "assert False\n# Raise=AssertionError()\n"
  },
  {
    "path": "crates/monty/test_cases/assert__fail_msg.py",
    "content": "assert False, 'custom message'\n# Raise=AssertionError('custom message')\n"
  },
  {
    "path": "crates/monty/test_cases/assert__fn_fail.py",
    "content": "# fmt: off\nassert(0)\n# Raise=AssertionError()\n"
  },
  {
    "path": "crates/monty/test_cases/assert__ops.py",
    "content": "# Tests for assert statements that pass (failure cases are in separate files)\n# === Basic assert ===\nassert True, 'basic assert True'\n\n# === Assert with expression ===\nassert 1 == 1, 'assert equality expression'\n\n# === Assert with function call style (assert is statement, not function) ===\n# fmt: off\nassert(123)\n# fmt: on\n"
  },
  {
    "path": "crates/monty/test_cases/async__asyncio_run.py",
    "content": "import asyncio\n\n\n# === Basic asyncio.run ===\nasync def simple():\n    return 42\n\n\nresult = asyncio.run(simple())\nassert result == 42, f'basic asyncio.run failed: {result}'\n\n\n# === With arguments ===\nasync def add(a, b):\n    return a + b\n\n\nresult = asyncio.run(add(10, 20))\nassert result == 30, f'asyncio.run with args failed: {result}'\n\n\n# === Nested awaits inside the coroutine ===\nasync def inner():\n    return 'hello'\n\n\nasync def outer():\n    val = await inner()\n    return val + ' world'\n\n\nresult = asyncio.run(outer())\nassert result == 'hello world', f'nested awaits failed: {result}'\n\n\n# === asyncio.gather inside asyncio.run ===\nasync def double(x):\n    return x * 2\n\n\nasync def run_gather():\n    results = await asyncio.gather(double(1), double(2), double(3))\n    return results\n\n\nresult = asyncio.run(run_gather())\nassert result == [2, 4, 6], f'gather inside run failed: {result}'\n"
  },
  {
    "path": "crates/monty/test_cases/async__basic.py",
    "content": "# run-async\n# Basic async function that returns a value\n\n\nasync def foo():\n    return 123\n\n\nresult = await foo()  # pyright: ignore\nassert result == 123, 'async function should return awaited value'\n"
  },
  {
    "path": "crates/monty/test_cases/async__closure.py",
    "content": "# run-async\n# Async function capturing variables from enclosing scope\n\n\ndef make_adder(n):\n    async def adder(x):\n        return x + n\n\n    return adder\n\n\nadd_five = make_adder(5)\nresult = await add_five(10)  # pyright: ignore\nassert result == 15, 'async closure should capture variables'\n"
  },
  {
    "path": "crates/monty/test_cases/async__double_await_coroutine.py",
    "content": "# run-async\nasync def foo():\n    return 1\n\n\ncoro = foo()\nawait coro  # pyright: ignore\nawait coro  # pyright: ignore\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"async__double_await_coroutine.py\", line 8, in <module>\n    await coro  # pyright: ignore\n    ~~~~~~~~~~\nRuntimeError: cannot reuse already awaited coroutine\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/async__exception.py",
    "content": "# run-async\n# Test that exceptions in async functions propagate correctly\n\n\nasync def raises_error():\n    raise ValueError('async error')\n\n\nawait raises_error()  # pyright: ignore\n# Raise=ValueError('async error')\n"
  },
  {
    "path": "crates/monty/test_cases/async__ext_call.py",
    "content": "# call-external\n# run-async\n# Test async external function calls (coroutines)\n\n# === Basic async external call ===\nresult = await async_call(42)  # pyright: ignore\nassert result == 42, 'async_call should return awaited value'\n\n# === Async call with string ===\ns = await async_call('hello')  # pyright: ignore\nassert s == 'hello', 'async_call should work with strings'\n\n# === Async call with list ===\nlst = await async_call([1, 2, 3])  # pyright: ignore\nassert lst == [1, 2, 3], 'async_call should work with lists'\n\n# === Multiple async calls ===\na = await async_call(10)  # pyright: ignore\nb = await async_call(20)  # pyright: ignore\nassert a + b == 30, 'multiple async calls should work'\n\n# === Gather multiple external async calls ===\nimport asyncio\n\nresults = await asyncio.gather(async_call(1), async_call(2), async_call(3))  # pyright: ignore\nassert results == [1, 2, 3], 'gather should collect external async results in order'\n\n# === Gather with mixed external calls ===\nresults = await asyncio.gather(async_call('a'), async_call('b'))  # pyright: ignore\nassert results == ['a', 'b'], 'gather should work with string returns'\n\n\n# === Gather mixing coroutines and external futures ===\nasync def add(a, b):\n    return a + b\n\n\nasync def multiply(a, b):\n    return a * b\n\n\n# Mix: coroutine first, external future second\nresults = await asyncio.gather(add(1, 2), async_call(10))  # pyright: ignore\nassert results == [3, 10], 'gather should work with coroutine then external future'\n\n# Mix: external future first, coroutine second\nresults = await asyncio.gather(async_call(20), multiply(3, 4))  # pyright: ignore\nassert results == [20, 12], 'gather should work with external future then coroutine'\n\n# Mix: multiple of each interleaved\nresults = await asyncio.gather(add(5, 5), async_call('x'), multiply(2, 3), async_call('y'))  # pyright: ignore\nassert results == [10, 'x', 6, 'y'], 'gather should handle interleaved coroutines and external futures'\n\n\n# === Coroutine with nested external awaits ===\nasync def double_external(x):\n    val = await async_call(x)\n    return val * 2\n\n\nresults = await asyncio.gather(double_external(5), async_call(100))  # pyright: ignore\nassert results == [10, 100], 'gather should work with coroutine that awaits external'\n\n\n# === Coroutine with multiple nested awaits ===\nasync def triple_add(a, b, c):\n    x = await async_call(a)\n    y = await async_call(b)\n    return x + y + c\n\n\nresults = await asyncio.gather(triple_add(1, 2, 3), async_call(50))  # pyright: ignore\nassert results == [6, 50], 'gather should work with coroutine with multiple external awaits'\n"
  },
  {
    "path": "crates/monty/test_cases/async__gather_all.py",
    "content": "# run-async\nimport asyncio\n\n\n# === Basic gather ===\nasync def task1():\n    return 1\n\n\nasync def task2():\n    return 2\n\n\nresult = await asyncio.gather(task1(), task2())  # pyright: ignore\nassert result == [1, 2], 'gather should return results as a list'\n\n\n# === Result ordering ===\n# Results should be in argument order, not completion order\nasync def slow():\n    return 'slow'\n\n\nasync def fast():\n    return 'fast'\n\n\nresult = await asyncio.gather(slow(), fast())  # pyright: ignore\nassert result == ['slow', 'fast'], 'gather should preserve argument order'\n\n# === Empty gather ===\nresult = await asyncio.gather()  # pyright: ignore\nassert result == [], 'empty gather should return empty list'\n\n\n# === Single coroutine ===\nasync def single():\n    return 42\n\n\nresult = await asyncio.gather(single())  # pyright: ignore\nassert result == [42], 'gather with single coroutine should return list with one element'\n\n# === repr of gather function ===\nr = repr(asyncio.gather)\nassert r.startswith('<function gather at 0x'), f'repr should start with: {r}'\n\n# === TypeError for non-awaitable argument ===\ntry:\n    await asyncio.gather(123)  # pyright: ignore\n    assert False, 'should have raised TypeError'\nexcept TypeError as e:\n    assert str(e) == 'An asyncio.Future, a coroutine or an awaitable is required'\n\n\n# === *args unpacking with gather ===\nasync def a():\n    return 'a'\n\n\nasync def b():\n    return 'b'\n\n\nasync def c():\n    return 'c'\n\n\n# Unpack a list of coroutines\ncoros = [a(), b(), c()]\nresult = await asyncio.gather(*coros)  # pyright: ignore\nassert result == ['a', 'b', 'c'], f'gather with *args unpacking: {result}'\n\n# Unpack with mixed args\nresult = await asyncio.gather(a(), *[b(), c()])  # pyright: ignore\nassert result == ['a', 'b', 'c'], f'gather with mixed args and *unpacking: {result}'\n\n# Unpack empty list\nresult = await asyncio.gather(*[])  # pyright: ignore\nassert result == [], f'gather with empty *args: {result}'\n\n# Unpack tuple\ncoro_tuple = (a(), b())\nresult = await asyncio.gather(*coro_tuple)  # pyright: ignore\nassert result == ['a', 'b'], f'gather with *tuple unpacking: {result}'\n"
  },
  {
    "path": "crates/monty/test_cases/async__nested_await.py",
    "content": "# run-async\n# Nested async function calls\n\n\nasync def inner():\n    return 42\n\n\nasync def outer():\n    value = await inner()\n    return value + 8\n\n\nresult = await outer()  # pyright: ignore\nassert result == 50, 'nested async calls should work'\n"
  },
  {
    "path": "crates/monty/test_cases/async__nested_gather_ext.py",
    "content": "# call-external\n# run-async\n# Test nested asyncio.gather where outer gather spawns tasks that each\n# do a sequential external await followed by an inner gather of external calls.\nimport asyncio\n\n\nasync def process_item(n):\n    base = await async_call(n * 10)\n    left = async_call(base + 1)\n    right = async_call(base + 2)\n    parts = await asyncio.gather(left, right)\n    return {'n': n, 'parts': parts}\n\n\nresults = await asyncio.gather(  # pyright: ignore\n    process_item(1), process_item(2), process_item(3)\n)\nassert results == [\n    {'n': 1, 'parts': [11, 12]},\n    {'n': 2, 'parts': [21, 22]},\n    {'n': 3, 'parts': [31, 32]},\n], f'nested gather with external calls: {results}'\n\n\n# === Nested gather with generator unpacking ===\nasync def fetch_pair(key):\n    val = await async_call(key)\n    return val\n\n\nasync def fetch_all(keys):\n    return await asyncio.gather(*(fetch_pair(k) for k in keys))\n\n\nouter = await asyncio.gather(fetch_all([1, 2]), fetch_all([3, 4]))  # pyright: ignore\nassert outer == [[1, 2], [3, 4]], f'nested gather with generator unpacking: {outer}'\n"
  },
  {
    "path": "crates/monty/test_cases/async__not_awaitable.py",
    "content": "# run-async\nawait 123  # pyright: ignore\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"async__not_awaitable.py\", line 2, in <module>\n    await 123  # pyright: ignore\n    ~~~~~~~~~\nTypeError: 'int' object can't be awaited\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/async__not_imported.py",
    "content": "# run-async\nasync def foo():\n    return 1\n\n\nawait asyncio.gather(foo(), foo())  # pyright: ignore\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"async__not_imported.py\", line 6, in <module>\n    await asyncio.gather(foo(), foo())  # pyright: ignore\n          ~~~~~~~\nNameError: name 'asyncio' is not defined. Did you forget to import 'asyncio'?\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/async__recursion_depth_isolation.py",
    "content": "# call-external\n# run-async\n# Test that recursion depth is per-task, not global.\n#\n# With a recursion limit of 50, a gathered task that recurses 40 deep\n# should NOT eat into another task's budget. Without per-task depth\n# tracking, the second task inherits the first task's depth and hits\n# the limit prematurely.\nimport asyncio\n\n\nasync def recurse_then_call(n):\n    \"\"\"Recurse n levels deep, then make an external call at the bottom.\"\"\"\n    if n == 0:\n        return await async_call('done')\n    return await recurse_then_call(n - 1)\n\n\n# Each task recurses 40 deep independently.\n# With a global depth counter, the second task would start at depth 40\n# and blow the limit at depth 80 (well above the 50 limit).\n# With correct per-task tracking, each task sees its own depth of 40.\nresults = await asyncio.gather(  # pyright: ignore\n    recurse_then_call(40),\n    recurse_then_call(40),\n)\nassert results == ['done', 'done'], f'both tasks should complete: {results}'\n"
  },
  {
    "path": "crates/monty/test_cases/async__return_types.py",
    "content": "# run-async\n# Async functions returning different types\n\n\nasync def return_int():\n    return 42\n\n\nasync def return_str():\n    return 'hello'\n\n\nasync def return_list():\n    return [1, 2, 3]\n\n\nasync def return_none():\n    pass\n\n\ni = await return_int()  # pyright: ignore\nassert i == 42, 'should return int'\n\ns = await return_str()  # pyright: ignore\nassert s == 'hello', 'should return str'\n\nlst = await return_list()  # pyright: ignore\nassert lst == [1, 2, 3], 'should return list'\n\nn = await return_none()  # pyright: ignore\nassert n is None, 'should return None implicitly'\n"
  },
  {
    "path": "crates/monty/test_cases/async__sequential.py",
    "content": "# run-async\n# Multiple sequential awaits\n\n\nasync def get_value(x):\n    return x * 2\n\n\na = await get_value(1)  # pyright: ignore\nb = await get_value(2)  # pyright: ignore\nc = await get_value(3)  # pyright: ignore\n\nassert a == 2, 'first await'\nassert b == 4, 'second await'\nassert c == 6, 'third await'\nassert a + b + c == 12, 'sum of sequential awaits'\n"
  },
  {
    "path": "crates/monty/test_cases/async__traceback.py",
    "content": "# run-async\n# Test that exceptions in async functions produce correct tracebacks\n\n\nasync def raises_error():\n    raise ValueError('async error')\n\n\nawait raises_error()  # pyright: ignore\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"async__traceback.py\", line 9, in <module>\n    await raises_error()  # pyright: ignore\n    ~~~~~~~~~~~~~~~~~~~~\n  File \"async__traceback.py\", line 6, in raises_error\n    raise ValueError('async error')\nValueError: async error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/async__with_args.py",
    "content": "# run-async\n# Async function with arguments\n\n\nasync def add(a, b):\n    return a + b\n\n\nresult = await add(10, 20)  # pyright: ignore\nassert result == 30, 'async function should handle arguments'\n\n# With keyword arguments\nresult2 = await add(a=5, b=15)  # pyright: ignore\nassert result2 == 20, 'async function should handle keyword arguments'\n"
  },
  {
    "path": "crates/monty/test_cases/attr__get_int_error.py",
    "content": "x = 5\nx.foo\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"attr__get_int_error.py\", line 2, in <module>\n    x.foo\nAttributeError: 'int' object has no attribute 'foo'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/attr__get_list_error.py",
    "content": "x = [1, 2, 3]\nx.foo\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"attr__get_list_error.py\", line 2, in <module>\n    x.foo\nAttributeError: 'list' object has no attribute 'foo'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/attr__set_frozen_nonfield.py",
    "content": "# call-external\n# Test that setting a non-field attribute on frozen dataclass raises error\npoint = make_point()\npoint.z = 42\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"attr__set_frozen_nonfield.py\", line 4, in <module>\n    point.z = 42\n    ~~~~~~~\nFrozenInstanceError: cannot assign to field 'z'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/attr__set_int_error.py",
    "content": "x = 5\nx.foo = 1\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"attr__set_int_error.py\", line 2, in <module>\n    x.foo = 1\n    ~~~~~\nAttributeError: 'int' object has no attribute 'foo' and no __dict__ for setting new attributes\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/attr__set_list_error.py",
    "content": "x = [1, 2, 3]\nx.foo = 1\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"attr__set_list_error.py\", line 2, in <module>\n    x.foo = 1\n    ~~~~~\nAttributeError: 'list' object has no attribute 'foo' and no __dict__ for setting new attributes\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/bench__kitchen_sink.py",
    "content": "# This test case is also used in the benchmark (benches/main.rs)\n# List operations\nmy_list = []\nmy_list.append(1)\nmy_list.append(2)\nmy_list.insert(0, 0)\nlist_len = len(my_list)\nlist_item = my_list[1]\n\n# Dict operations\nmy_dict = {}\nmy_dict['a'] = 10\nmy_dict['b'] = 20\ndict_val = my_dict['a']\npopped = my_dict.pop('b')\ndict_len = len(my_dict)\n\n# Tuple operations\nmy_tuple = (1, 2, 3)\ntuple_item = my_tuple[0]\ntuple_len = len(my_tuple)\n\n# String operations\ns = 'hello'\ns += ' world'\nstr_len = len(s)\n\n\n# Function definition and call\ndef add(x, y):\n    return x + y\n\n\nfunc_result = add(3, 4)\n\n# For loop with if/elif/else\ntotal = 0\nfor i in range(10):\n    if i < 3:\n        total += 1\n    elif i < 6:\n        total += 2\n    else:\n        total += 3\n\n# Boolean operators and comparisons\nflag = True and not False\ncheck = 1 < 2 and 3 > 2\nidentity = None is None\nnot_identity = 1 is not None\ncompare = 5 >= 5 and 5 <= 5 and 4 != 5\n\n# Assert with message\nassert total > 0, 'total should be positive'\n\n# List comprehension\nsquares = [x * x for x in range(10)]\ncomp_sum = sum(squares)\n\n# Dict comprehension\nsquare_dict = {x: x * x for x in range(5)}\ndict_comp_sum = sum(square_dict.values())\n\n# Final result\nresult = list_len + list_item + dict_val + dict_len + tuple_item + tuple_len\nresult += str_len + func_result + total + comp_sum + dict_comp_sum\nresult\n# Return=373\n"
  },
  {
    "path": "crates/monty/test_cases/bool__ops.py",
    "content": "# === Boolean 'and' operator ===\n# returns first falsy value, or last value if all truthy\nassert (5 and 3) == 3, 'and truthy'\nassert (0 and 3) == 0, 'and falsy'\nassert (1 and 2 and 3) == 3, 'and chained'\n\n# === Boolean 'or' operator ===\n# returns first truthy value, or last value if all falsy\nassert (5 or 3) == 5, 'or truthy'\nassert (0 or 3) == 3, 'or falsy'\nassert (0 or 0 or 3) == 3, 'or chained'\n\n# === Boolean 'not' operator ===\nassert (not 5) == False, 'not truthy'\nassert (not 0) == True, 'not falsy'\nassert (not None) == True, 'not None'\n\n# === Complex boolean expressions ===\nassert ((1 and 2) or (3 and 0)) == 2, 'complex and/or'\nassert (not (0 and 1)) == True, 'not and combined'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__add_type_error.py",
    "content": "len + 1\n# Raise=TypeError(\"unsupported operand type(s) for +: 'builtin_function_or_method' and 'int'\")\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__filter.py",
    "content": "assert list(filter(None, [0, 1, False, True, '', 'hello'])) == [1, True, 'hello'], 'filter None removes falsy values'\nassert list(filter(None, [])) == [], 'filter None on empty list'\nassert list(filter(None, [0, 0, 0])) == [], 'filter None removes all zeros'\nassert list(filter(None, [1, 2, 3])) == [1, 2, 3], 'filter None keeps truthy values'\nassert list(filter(None, ['', '', 'x'])) == ['x'], 'filter None keeps non-empty string'\n\nassert list(filter(abs, [-1, 0, 1])) == [-1, 1], 'filter with abs keeps non-zero'\nassert list(filter(abs, [0, 0, 0])) == [], 'filter with abs removes zeros'\nassert list(filter(abs, [-5, -3, 0, 2, 0, 4])) == [-5, -3, 2, 4], 'filter with abs mixed'\n\nassert list(filter(bool, [0, 1, '', 'x'])) == [1, 'x'], 'filter with bool'\nassert list(filter(bool, [False, True, 0, 1])) == [True, 1], 'filter with bool booleans and ints'\nassert list(filter(bool, [[], [1], (), (2,)])) == [[1], (2,)], 'filter with bool containers'\n\n# Note: len returns int, so empty containers return 0 (falsy), non-empty return truthy\nassert list(filter(len, ['', 'a', '', 'bc'])) == ['a', 'bc'], 'filter with len on strings'\nassert list(filter(len, [[], [1], [], [2, 3]])) == [[1], [2, 3]], 'filter with len on lists'\nassert list(filter(len, [(), (1,), (), (2, 3)])) == [(1,), (2, 3)], 'filter with len on tuples'\n\nassert list(filter(int, ['0', '1', '2', '0'])) == ['1', '2'], 'filter with int on string numbers'\nassert list(filter(int, [0.0, 1.5, 0.0, 2.3])) == [1.5, 2.3], 'filter with int on floats'\n\nassert list(filter(str, [0, 1, '', 'x'])) == [0, 1, 'x'], 'filter with str converts and checks truthiness'\n\nassert list(filter(None, [1, 2, 3])) == [1, 2, 3], 'filter list'\n\nassert list(filter(None, (0, 1, 2))) == [1, 2], 'filter tuple'\n\nassert list(filter(None, 'abc')) == ['a', 'b', 'c'], 'filter string'\nassert list(filter(None, 'a b')) == ['a', ' ', 'b'], 'filter string with space'\n\nassert list(filter(None, range(0, 5))) == [1, 2, 3, 4], 'filter range'\nassert list(filter(None, range(1, 4))) == [1, 2, 3], 'filter range all truthy'\n\nassert list(filter(None, {0, 1, 2})) == [1, 2] or list(filter(None, {0, 1, 2})) == [2, 1], 'filter set'\n\nassert list(filter(None, [])) == [], 'filter empty list'\nassert list(filter(None, ())) == [], 'filter empty tuple'\nassert list(filter(None, '')) == [], 'filter empty string'\nassert list(filter(None, range(0))) == [], 'filter empty range'\n\nassert list(filter(None, [[], [1], []])) == [[1]], 'filter nested lists'\nassert list(filter(None, [(), (1,), ()])) == [(1,)], 'filter nested tuples'\n\n\n# filter() with user-defined function\n# This should error until user-defined functions are supported\ndef is_positive(x):\n    return x > 0\n\n\nassert list(filter(is_positive, [-1, 1])) == [1], 'filter with user-defined function keeps positives'\n\n\nassert list(filter(lambda x: x > 0, [-1, 1])) == [1], 'filter with lambda keeps positives'\n\n\ntry:\n    list(filter(4, [1, 2]))\n    assert False, 'filter with non-callable first argument should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == \"'int' object is not callable\", 'filter with non-callable first argument raises TypeError'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__filter_not_iterable.py",
    "content": "# filter() with non-iterable second argument\nfilter(None, 42)\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"builtin__filter_not_iterable.py\", line 2, in <module>\n    filter(None, 42)\n    ~~~~~~~~~~~~~~~~\nTypeError: 'int' object is not iterable\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__getattr.py",
    "content": "# Test getattr() builtin function\n\ns = slice(1, 10, 2)\nassert getattr(s, 'start') == 1, 'getattr(slice, \"start\") should return 1'\nassert getattr(s, 'stop') == 10, 'getattr(slice, \"stop\") should return 10'\nassert getattr(s, 'step') == 2, 'getattr(slice, \"step\") should return 2'\n\nassert getattr(s, 'nonexistent', 'default') == 'default', 'getattr with default should return default'\nassert getattr(s, 'nonexistent', None) == None, 'getattr with None default should return None'\nassert getattr(s, 'nonexistent', 42) == 42, 'getattr with numeric default should return number'\n\nassert getattr(s, 'start', 999) == 1, 'getattr should return actual value, not default'\n\ntry:\n    getattr(s, 'nonexistent')\n    assert False, 'getattr should raise AttributeError for missing attribute'\nexcept AttributeError:\n    pass\n\ntry:\n    getattr()\n    assert False, 'getattr() with no args should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == 'getattr expected at least 2 arguments, got 0', str(e)\n\ntry:\n    getattr(kwarg=1)\n    assert False, 'getattr() with keyword arg should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == 'getattr() takes no keyword arguments', str(e)\n\ntry:\n    getattr(s)\n    assert False, 'getattr() with 1 arg should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == 'getattr expected at least 2 arguments, got 1', str(e)\n\ntry:\n    getattr(s, 'start', 'default', 'extra')\n    assert False, 'getattr() with 4 args should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == 'getattr expected at most 3 arguments, got 4', str(e)\n\ntry:\n    getattr(s, 123)\n    assert False, 'getattr() with non-string name should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == \"attribute name must be string, not 'int'\", str(e)\n\ntry:\n    getattr(s, None)\n    assert False, 'getattr() with None name should raise TypeError'\nexcept TypeError as e:\n    assert str(e) == \"attribute name must be string, not 'NoneType'\", str(e)\n\ntry:\n    raise ValueError('test error')\nexcept ValueError as e:\n    args = getattr(e, 'args')\n    assert args == ('test error',), 'exception args should be accessible via getattr'\n\n# === Dynamic (heap-allocated) attribute name strings ===\n# These test that getattr works with non-interned strings (e.g. from concatenation)\ns2 = slice(5, 15, 3)\nattr_name = 'sta' + 'rt'\nassert getattr(s2, attr_name) == 5, 'getattr with concatenated string should work'\n\nattr_name = 'st' + 'op'\nassert getattr(s2, attr_name) == 15, 'getattr with concatenated \"stop\" should work'\n\nattr_name = 'st' + 'ep'\nassert getattr(s2, attr_name) == 3, 'getattr with concatenated \"step\" should work'\n\n# Dynamic attribute name with default for missing attribute\nattr_name = 'non' + 'existent'\nassert getattr(s2, attr_name, 42) == 42, 'getattr with dynamic missing attr should return default'\n\n# Dynamic attribute name on exception\ntry:\n    raise TypeError('dynamic test')\nexcept TypeError as e:\n    attr_name = 'ar' + 'gs'\n    args = getattr(e, attr_name)\n    assert args == ('dynamic test',), 'exception args via dynamic string should work'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__iter_err_unpack_int.py",
    "content": "iterator = 0\n\niter(**42)\n# Raise=TypeError('iter() argument after ** must be a mapping, not int')\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__iter_funcs.py",
    "content": "# === sum() ===\n# Basic sum operations\nassert sum([1, 2, 3]) == 6, 'sum of list'\nassert sum([1, 2, 3], 10) == 16, 'sum with start value'\nassert sum(()) == 0, 'sum of empty tuple'\nassert sum([], 5) == 5, 'sum of empty list with start'\nassert sum(range(5)) == 10, 'sum of range'\nassert sum([1.5, 2.5, 3.0], 0.0) == 7.0, 'sum of floats with float start'\n# Note: sum of floats without start requires py_add to support int+float\n\n# sum with different iterables\nassert sum({1, 2, 3}) == 6, 'sum of set'\nassert sum({1: 'a', 2: 'b', 3: 'c'}) == 6, 'sum of dict keys'\n\n# === any() ===\n# Basic any operations\nassert any([True, False, False]) == True, 'any with one True'\nassert any([False, False, False]) == False, 'any with all False'\nassert any([]) == False, 'any of empty list'\nassert any([0, 0, 1]) == True, 'any with truthy int'\nassert any([0, '', None]) == False, 'any with all falsy'\nassert any(['', 'hello']) == True, 'any with non-empty string'\nassert any(range(0, 5)) == True, 'any of range (has non-zero)'\nassert any(range(0, 1)) == False, 'any of range(0,1) is False (only 0)'\n\n# === all() ===\n# Basic all operations\nassert all([True, True, True]) == True, 'all with all True'\nassert all([True, False, True]) == False, 'all with one False'\nassert all([]) == True, 'all of empty list'\nassert all([1, 2, 3]) == True, 'all with truthy ints'\nassert all([1, 0, 3]) == False, 'all with zero'\nassert all(['a', 'b', 'c']) == True, 'all with non-empty strings'\nassert all(['a', '', 'c']) == False, 'all with empty string'\n\n# More edge cases with nested structures\nassert any([[1], [], [3]]) == True, 'any with nested lists (some non-empty)'\nassert all([[1], [2], [3]]) == True, 'all with non-empty nested lists'\n\n# sum with lists (list + list is supported)\nassert sum([[1], [2], [3]], []) == [1, 2, 3], 'sum lists with empty start'\n# Note: sum with tuples requires Tuple py_add which is not implemented\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__iter_next.py",
    "content": "# === iter() on various iterables ===\n# iter() creates an iterator from an iterable\n\n# iter() on list\nit = iter([1, 2, 3])\nassert next(it) == 1, 'iter list: first element should be 1'\nassert next(it) == 2, 'iter list: second element should be 2'\nassert next(it) == 3, 'iter list: third element should be 3'\n\n# iter() on tuple\nit = iter((10, 20))\nassert next(it) == 10, 'iter tuple: first element should be 10'\nassert next(it) == 20, 'iter tuple: second element should be 20'\n\n# iter() on string\nit = iter('ab')\nassert next(it) == 'a', 'iter string: first element should be a'\nassert next(it) == 'b', 'iter string: second element should be b'\n\n# iter() on range\nit = iter(range(3))\nassert next(it) == 0, 'iter range: first element should be 0'\nassert next(it) == 1, 'iter range: second element should be 1'\nassert next(it) == 2, 'iter range: third element should be 2'\n\n# iter() on dict iterates over keys\nd = {'x': 1, 'y': 2}\nit = iter(d)\nkeys = [next(it), next(it)]\nassert 'x' in keys, 'iter dict: x should be in keys'\nassert 'y' in keys, 'iter dict: y should be in keys'\n\n# === next() with default value ===\n# next() returns default when iterator is exhausted\n\nit = iter([42])\nassert next(it) == 42, 'next: first element should be 42'\nassert next(it, 'done') == 'done', 'next with default: should return default when exhausted'\n\n# Check default with various types\nit = iter([])\nassert next(it, None) is None, 'next with None default: should return None'\nassert next(it, 0) == 0, 'next with 0 default: should return 0'\nassert next(it, []) == [], 'next with empty list default: should return empty list'\n\n# === iter() on iterator returns itself ===\n# Calling iter() on an iterator should return the same iterator\n\noriginal = iter([1, 2, 3])\nsame = iter(original)\n# They should iterate over the same values\nassert next(original) == 1, 'iter on iterator: original first should be 1'\nassert next(same) == 2, 'iter on iterator: same should continue from 2'\nassert next(original) == 3, 'iter on iterator: original should continue to 3'\n\n# === Multiple independent iterators ===\n# Creating multiple iterators over the same iterable should be independent\n\ndata = [1, 2, 3]\nit1 = iter(data)\nit2 = iter(data)\nassert next(it1) == 1, 'independent iterators: it1 first should be 1'\nassert next(it1) == 2, 'independent iterators: it1 second should be 2'\nassert next(it2) == 1, 'independent iterators: it2 first should be 1 (independent)'\nassert next(it1) == 3, 'independent iterators: it1 third should be 3'\nassert next(it2) == 2, 'independent iterators: it2 second should be 2'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__map.py",
    "content": "assert list(map(abs, [-1, 0, 1, -2])) == [1, 0, 1, 2], 'map with abs'\nassert list(map(abs, [0, 0, 0])) == [0, 0, 0], 'map with abs all zeros'\n\nassert list(map(str, [1, 2, 3])) == ['1', '2', '3'], 'map with str on ints'\nassert list(map(str, [True, False])) == ['True', 'False'], 'map with str on bools'\n\nassert list(map(int, ['1', '2', '3'])) == [1, 2, 3], 'map with int on strings'\nassert list(map(int, [1.1, 2.9, 3.5])) == [1, 2, 3], 'map with int on floats'\nassert list(map(int, [True, False, True])) == [1, 0, 1], 'map with int on bools'\n\nassert list(map(bool, [0, 1, '', 'x'])) == [False, True, False, True], 'map with bool'\nassert list(map(bool, [[], [1], (), (2,)])) == [False, True, False, True], 'map with bool on containers'\n\nassert list(map(len, ['', 'a', 'ab', 'abc'])) == [0, 1, 2, 3], 'map with len on strings'\nassert list(map(len, [[], [1], [1, 2], [1, 2, 3]])) == [0, 1, 2, 3], 'map with len on lists'\n\nassert list(map(float, [1, 2, 3])) == [1.0, 2.0, 3.0], 'map with float on ints'\nassert list(map(float, ['1.5', '2.5'])) == [1.5, 2.5], 'map with float on strings'\n\nassert list(map(abs, [1, -2, 3])) == [1, 2, 3], 'map on list'\n\nassert list(map(abs, (1, -2, 3))) == [1, 2, 3], 'map on tuple'\n\nassert list(map(ord, 'abc')) == [97, 98, 99], 'map ord on string'\n\nassert list(map(abs, range(-3, 3))) == [3, 2, 1, 0, 1, 2], 'map on range'\n\nresult = list(map(abs, {-1, 0, 1}))\nassert sorted(result) == [0, 1, 1], 'map on set'\n\nassert list(map(abs, [])) == [], 'map on empty list'\nassert list(map(abs, ())) == [], 'map on empty tuple'\nassert list(map(abs, '')) == [], 'map on empty string'\nassert list(map(abs, range(0))) == [], 'map on empty range'\n\nassert list(map(list, [(1, 2), (3, 4)])) == [[1, 2], [3, 4]], 'map with list constructor'\nassert list(map(tuple, [[1, 2], [3, 4]])) == [(1, 2), (3, 4)], 'map with tuple constructor'\n\nassert list(map(pow, [2, 3, 4], [3, 2, 2])) == [8, 9, 16], 'map with pow and 2 iterables'\n\nassert list(map(divmod, [10, 20, 30], [3, 6, 7])) == [(3, 1), (3, 2), (4, 2)], 'map with divmod and 2 iterables'\n\nassert list(map(pow, [2, 3, 4, 5], [3, 2])) == [8, 9], 'map stops at shortest iterable'\nassert list(map(pow, [2, 3], [3, 2, 1, 0])) == [8, 9], 'map stops at shortest iterable (first shorter)'\n\nassert list(map(pow, [2], [3, 4, 5])) == [8], 'map with single item in shortest'\n\n\ndef f(x):\n    return x * 2\n\n\nassert list(map(f, [1, 2, 3])) == [2, 4, 6], 'map with custom function'\n\n\ndef raise_exception(x):\n    raise ValueError('Intentional error')\n\n\ntry:\n    list(map(raise_exception, [1, 2, 3]))\n    assert False, 'should have failed with exception'\nexcept ValueError as e:\n    assert str(e) == 'Intentional error', 'map with function that raises exception'\n\ntry:\n    map()\nexcept TypeError as e:\n    assert str(e) == 'map() must have at least two arguments.', 'map with no arguments'\n\ntry:\n    map(None)\nexcept TypeError as e:\n    assert str(e) == 'map() must have at least two arguments.', 'map with only function argument'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__map_not_iterable.py",
    "content": "# map() with non-iterable second argument\nmap(abs, 42)\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"builtin__map_not_iterable.py\", line 2, in <module>\n    map(abs, 42)\n    ~~~~~~~~~~~~\nTypeError: 'int' object is not iterable\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__math_funcs.py",
    "content": "# === abs() ===\n# Basic abs operations\nassert abs(5) == 5, 'abs of positive int'\nassert abs(-5) == 5, 'abs of negative int'\nassert abs(0) == 0, 'abs of zero'\nassert abs(3.14) == 3.14, 'abs of positive float'\nassert abs(-3.14) == 3.14, 'abs of negative float'\nassert abs(True) == 1, 'abs of True'\nassert abs(False) == 0, 'abs of False'\n\n# === round() ===\n# Basic round operations\nassert round(2.5) == 2, 'round 2.5 (bankers rounding)'\nassert round(3.5) == 4, 'round 3.5 (bankers rounding)'\nassert round(0.5) == 0, 'round 0.5 (bankers rounding)'\nassert round(-0.5) == 0, 'round -0.5 (bankers rounding)'\nassert round(2.4) == 2, 'round 2.4'\nassert round(2.6) == 3, 'round 2.6'\nassert round(-2.5) == -2, 'round -2.5'\nassert round(-1.5) == -2, 'round -1.5 (bankers rounding)'\nassert round(5) == 5, 'round integer'\n\n# round with ndigits\nassert round(3.14159, 2) == 3.14, 'round to 2 digits'\nassert round(3.14159, 0) == 3.0, 'round to 0 digits returns float'\nassert repr(round(-0.4, 0)) == '-0.0', 'round(-0.4, 0) preserves negative zero sign'\nassert repr(round(-0.5, 0)) == '-0.0', 'round(-0.5, 0) preserves negative zero sign'\nassert round(1234, -2) == 1200, 'round int to nearest 100'\nassert round(1250, -2) == 1200, 'round 1250 to nearest 100 (bankers)'\nassert round(1350, -2) == 1400, 'round 1350 to nearest 100'\nassert round(15, -1) == 20, 'round 15 to nearest 10 (bankers)'\nassert round(25, -1) == 20, 'round 25 to nearest 10 (bankers)'\n\n# round with None\nassert round(2.5, None) == 2, 'round with None ndigits'\nassert round(True, -1) == 0, 'round True with negative digits behaves like int'\nassert round(True, 2) == 1, 'round True with positive digits returns int'\nassert round(False, -3) == 0, 'round False with negative digits stays zero'\n\n# round type errors\nthrew = False\ntry:\n    round(1.2, 1.5)\nexcept TypeError:\n    threw = True\nassert threw, 'round with non-int ndigits raises TypeError'\n\n# round edge cases with extreme values\nassert isinstance(round(1e15), int), 'round large float returns int'\nassert isinstance(round(-1e15), int), 'round large negative float returns int'\nassert round(0.0) == 0, 'round(0.0) is zero'\nassert round(-0.0) == 0, 'round(-0.0) is zero'\n\n# round special float values (infinity / NaN)\ninf = float('inf')\nneg_inf = float('-inf')\nnan = float('nan')\n\nthrew = False\ntry:\n    round(inf)\nexcept OverflowError:\n    threw = True\nassert threw, 'round(inf) raises OverflowError'\n\nthrew = False\ntry:\n    round(neg_inf)\nexcept OverflowError:\n    threw = True\nassert threw, 'round(-inf) raises OverflowError'\n\nthrew = False\ntry:\n    round(nan)\nexcept ValueError:\n    threw = True\nassert threw, 'round(nan) raises ValueError'\n\nr = round(inf, 0)\nassert r == inf, 'round(inf, 0) returns inf'\n\nr = round(neg_inf, 0)\nassert r == neg_inf, 'round(-inf, 0) returns -inf'\n\nr = round(nan, 0)\nassert r != r, 'round(nan, 0) returns NaN'\n\n# round with extreme ndigits values\nassert round(1.23, 10**6) == 1.23, 'round with huge positive ndigits returns original float'\nassert round(1.23, -(10**6)) == 0.0, 'round with huge negative ndigits returns zero'\nassert repr(round(-1.23, -(10**6))) == '-0.0', 'round with huge negative ndigits preserves signed zero'\n\n# round with float result (ndigits specified)\nassert isinstance(round(1.5, 1), float), 'round with ndigits returns float'\nassert round(1.25, 1) == 1.2, 'round 1.25 to 1 decimal (bankers rounding)'\nassert round(1.35, 1) == 1.4, 'round 1.35 to 1 decimal'\n\n# === divmod() ===\n# Basic divmod operations\nassert divmod(17, 5) == (3, 2), 'divmod 17, 5'\nassert divmod(10, 3) == (3, 1), 'divmod 10, 3'\nassert divmod(9, 3) == (3, 0), 'divmod 9, 3'\nassert divmod(-10, 3) == (-4, 2), 'divmod -10, 3 (floor division)'\nassert divmod(10, -3) == (-4, -2), 'divmod 10, -3'\nassert divmod(-10, -3) == (3, -1), 'divmod -10, -3'\n\n# divmod with floats\nr = divmod(7.5, 2.5)\nassert r[0] == 3.0 and r[1] == 0.0, 'divmod floats'\nassert divmod(True, 2) == (0, 1), 'divmod accepts bool numerator'\nassert divmod(5, True) == (5, 0), 'divmod accepts bool denominator'\n\n# === pow() ===\n# Basic pow operations\nassert pow(2, 3) == 8, 'pow 2^3'\nassert pow(2, 0) == 1, 'pow x^0'\nassert pow(5, 1) == 5, 'pow x^1'\nassert pow(2, 10) == 1024, 'pow 2^10'\n\n# pow with negative exponent\nassert pow(2, -1) == 0.5, 'pow with negative exp'\nassert pow(4, -2) == 0.0625, 'pow 4^-2'\n\n# pow with floats\nassert pow(2.0, 3.0) == 8.0, 'pow with floats'\nassert pow(4.0, 0.5) == 2.0, 'pow float sqrt'\n\n# Three-argument pow (modular exponentiation)\nassert pow(2, 10, 1000) == 24, 'pow modular 2^10 % 1000'\nassert pow(3, 4, 5) == 1, 'pow modular 3^4 % 5'\nassert pow(7, 256, 13) == 9, 'pow modular large exp'\n\n# Modular exponentiation edge cases\nassert pow(2, 0, 5) == 1, 'pow x^0 mod n'\nassert pow(0, 5, 3) == 0, 'pow 0^n mod m'\nassert pow(True, 2) == 1, 'pow handles bool base'\nassert pow(2, True) == 2, 'pow handles bool exponent'\nassert pow(True, True) == 1, 'pow handles bool base and exponent'\nassert pow(True, -1) == 1.0, 'pow bool negative exponent works like int'\n\nthrew = False\ntry:\n    pow(0, -1)\nexcept ZeroDivisionError:\n    threw = True\nassert threw, 'pow(0, negative) raises ZeroDivisionError'\n\nthrew = False\ntry:\n    pow(0.0, -1)\nexcept ZeroDivisionError:\n    threw = True\nassert threw, 'pow(0.0, negative) raises ZeroDivisionError'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__more_iter_funcs.py",
    "content": "# === min() ===\n# Basic min operations\nassert min([1, 2, 3]) == 1, 'min of list'\nassert min([3, 1, 2]) == 1, 'min of unsorted list'\nassert min([5]) == 5, 'min of single element'\nassert min(1, 2, 3) == 1, 'min of multiple args'\nassert min(3, 1, 2) == 1, 'min of unsorted args'\nassert min(-5, -10, -1) == -10, 'min of negatives'\n\n# min with strings\nassert min(['b', 'a', 'c']) == 'a', 'min of string list'\nassert min('b', 'a', 'c') == 'a', 'min of string args'\n\n# min with floats\nassert min([1.5, 0.5, 2.5]) == 0.5, 'min of floats'\nassert min(1.5, 0.5) == 0.5, 'min float args'\n\n# === max() ===\n# Basic max operations\nassert max([1, 2, 3]) == 3, 'max of list'\nassert max([3, 1, 2]) == 3, 'max of unsorted list'\nassert max([5]) == 5, 'max of single element'\nassert max(1, 2, 3) == 3, 'max of multiple args'\nassert max(3, 1, 2) == 3, 'max of unsorted args'\nassert max(-5, -10, -1) == -1, 'max of negatives'\n\n# max with strings\nassert max(['b', 'a', 'c']) == 'c', 'max of string list'\nassert max('b', 'a', 'c') == 'c', 'max of string args'\n\n# max with floats\nassert max([1.5, 0.5, 2.5]) == 2.5, 'max of floats'\nassert max(1.5, 2.5) == 2.5, 'max float args'\n\n# max with keyword arguments\nassert max([3, -1, 2, -4], key=abs) == -4, 'max key=abs'\nassert max(['a', 'bbb', 'cc'], key=len) == 'bbb', 'max key=len'\nassert max(['a', 'bbb', 'cc'], key=lambda s: len(s)) == 'bbb', 'max key=lambda simple callable'\nassert max('a', 'bbb', 'cc', key=len) == 'bbb', 'max multiple args key=len'\nassert max([1, 2, 3], key=None) == 3, 'max key=None same as no key'\nassert max([], default='fallback') == 'fallback', 'max default for empty iterable'\nassert max([], key=len, default='fallback') == 'fallback', 'max key+default for empty iterable'\n\n# min with keyword arguments\nassert min([3, -1, 2, -4], key=abs) == -1, 'min key=abs'\nassert min(['a', 'bbb', 'cc'], key=len) == 'a', 'min key=len'\nassert min(['a', 'bbb', 'cc'], key=lambda s: len(s)) == 'a', 'min key=lambda simple callable'\nassert min('a', 'bbb', 'cc', key=len) == 'a', 'min multiple args key=len'\nassert min([1, 2, 3], key=None) == 1, 'min key=None same as no key'\nassert min([], default='fallback') == 'fallback', 'min default for empty iterable'\nassert min([], key=len, default='fallback') == 'fallback', 'min key+default for empty iterable'\n\n# max/min with tuple-producing key functions\nranked_items = [\n    {'downloads': 10, 'likes': 1},\n    {'downloads': 10, 'likes': 5},\n    {'downloads': 20, 'likes': 0},\n]\nassert max(ranked_items, key=lambda item: (item.get('downloads', 0), item.get('likes', 0))) == {\n    'downloads': 20,\n    'likes': 0,\n}, 'max key=lambda tuple ranking'\n\ntie_items = [\n    {'downloads': 10, 'likes': 5, 'name': 'first'},\n    {'downloads': 10, 'likes': 5, 'name': 'second'},\n]\nassert max(tie_items, key=lambda item: (item['downloads'], item['likes']))['name'] == 'first', (\n    'max returns first maximal item on ties'\n)\nassert min(tie_items, key=lambda item: (item['downloads'], item['likes']))['name'] == 'first', (\n    'min returns first minimal item on ties'\n)\n\ntry:\n    max([1], nope=1)\n    assert False, 'invalid max keyword should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"max() got an unexpected keyword argument 'nope'\",), 'max invalid keyword error matches CPython'\n\ntry:\n    min([1], nope=1)\n    assert False, 'invalid min keyword should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"min() got an unexpected keyword argument 'nope'\",), 'min invalid keyword error matches CPython'\n\ntry:\n    max(key=int)\n    assert False, 'max with only kwargs should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('max expected at least 1 argument, got 0',), 'max kwargs-only arity error matches CPython'\n\ntry:\n    min(default=None, key=int)\n    assert False, 'min with only kwargs should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('min expected at least 1 argument, got 0',), 'min kwargs-only arity error matches CPython'\n\ntry:\n    max(nope=1)\n    assert False, 'max with only unexpected kwargs should still raise the zero-arg TypeError'\nexcept TypeError as e:\n    assert e.args == ('max expected at least 1 argument, got 0',), (\n        'max zero-arg error takes precedence over kwargs validation'\n    )\n\ntry:\n    min(nope=1)\n    assert False, 'min with only unexpected kwargs should still raise the zero-arg TypeError'\nexcept TypeError as e:\n    assert e.args == ('min expected at least 1 argument, got 0',), (\n        'min zero-arg error takes precedence over kwargs validation'\n    )\n\ntry:\n    max(key=int, nope=1)\n    assert False, 'max with mixed kwargs and no positional args should still raise the zero-arg TypeError'\nexcept TypeError as e:\n    assert e.args == ('max expected at least 1 argument, got 0',), 'max zero-arg error beats mixed kwargs validation'\n\ntry:\n    max(1, 2, default=3)\n    assert False, 'max with multiple args and default should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('Cannot specify a default for max() with multiple positional arguments',), (\n        'max multiple args default error matches CPython'\n    )\n\ntry:\n    min(1, 2, default=3)\n    assert False, 'min with multiple args and default should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('Cannot specify a default for min() with multiple positional arguments',), (\n        'min multiple args default error matches CPython'\n    )\n\ntry:\n    max(1, key=int)\n    assert False, 'max single non-iterable arg with key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"'int' object is not iterable\",), 'max single arg with key still uses iterable form'\n\ntry:\n    min(1, key=int)\n    assert False, 'min single non-iterable arg with key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"'int' object is not iterable\",), 'min single arg with key still uses iterable form'\n\ntry:\n    max([1], key=1)\n    assert False, 'max non-callable key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"'int' object is not callable\",), 'max non-callable key error matches CPython'\n\ntry:\n    min([1], key=1)\n    assert False, 'min non-callable key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"'int' object is not callable\",), 'min non-callable key error matches CPython'\n\ntry:\n    max([])\n    assert False, 'max empty iterable without default should raise ValueError'\nexcept ValueError as e:\n    assert e.args == ('max() iterable argument is empty',), 'max empty iterable error unchanged'\n\ntry:\n    min([])\n    assert False, 'min empty iterable without default should raise ValueError'\nexcept ValueError as e:\n    assert e.args == ('min() iterable argument is empty',), 'min empty iterable error unchanged'\n\nassert max([1], default=2) == 1, 'max ignores default for non-empty iterable'\nassert min([1], default=2) == 1, 'min ignores default for non-empty iterable'\nassert max([], key=1, default='fallback') == 'fallback', 'max does not validate key for empty iterable with default'\nassert min([], key=1, default='fallback') == 'fallback', 'min does not validate key for empty iterable with default'\n\ntry:\n    max([1], key=abs, **{'key': len})\n    assert False, 'duplicate max key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"max() got multiple values for keyword argument 'key'\",), (\n        'max duplicate key error matches CPython'\n    )\n\ntry:\n    min([], default='x', **{'default': 'y'})\n    assert False, 'duplicate min default should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"min() got multiple values for keyword argument 'default'\",), (\n        'min duplicate default error matches CPython'\n    )\n\ntry:\n    max([], **{1: 2})\n    assert False, 'max non-string keyword key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('keywords must be strings',), 'max non-string keyword key error matches CPython'\n\ntry:\n    max([1, 'a'])\n    assert False, 'max with incomparable iterable items should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"'>' not supported between instances of 'str' and 'int'\",), (\n        'max iterable comparison error matches CPython'\n    )\n\ntry:\n    min(1, 'a')\n    assert False, 'min with incomparable positional args should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"'<' not supported between instances of 'str' and 'int'\",), (\n        'min positional comparison error matches CPython'\n    )\n\nmax_key_map = {10: 1, 20: 3, 30: 3, 40: 2}\nassert max([10, 20, 30, 40], key=lambda item: max_key_map[item]) == 20, (\n    'max returns first item among repeated maximal keys'\n)\n\nmin_key_map = {10: 2, 20: 1, 30: 1, 40: 3}\nassert min([10, 20, 30, 40], key=lambda item: min_key_map[item]) == 20, (\n    'min returns first item among repeated minimal keys'\n)\n\n# === sorted() ===\n# Basic sorted operations\nassert sorted([3, 1, 2]) == [1, 2, 3], 'sorted int list'\nassert sorted([1, 2, 3]) == [1, 2, 3], 'sorted already sorted'\nassert sorted([3, 2, 1]) == [1, 2, 3], 'sorted reverse order'\nassert sorted([]) == [], 'sorted empty list'\nassert sorted([5]) == [5], 'sorted single element'\n\n# sorted with strings\nassert sorted(['c', 'a', 'b']) == ['a', 'b', 'c'], 'sorted strings'\n\n# sorted with heap-allocated strings (from split)\nassert sorted('banana,apple,cherry'.split(',')) == ['apple', 'banana', 'cherry'], 'sorted split strings'\n\n# sorted with multi-char string literals (heap-allocated)\nassert sorted(['banana', 'apple', 'cherry']) == ['apple', 'banana', 'cherry'], 'sorted multi-char strings'\n\n# min/max with heap-allocated strings\nassert min('banana,apple,cherry'.split(',')) == 'apple', 'min of split strings'\nassert max('banana,apple,cherry'.split(',')) == 'cherry', 'max of split strings'\n\n# sorted with negative numbers\nassert sorted([-3, 1, -2, 2]) == [-3, -2, 1, 2], 'sorted with negatives'\n\n# sorted with tuple\nassert sorted((3, 1, 2)) == [1, 2, 3], 'sorted tuple returns list'\n\n# sorted preserves duplicates\nassert sorted([3, 1, 2, 1, 3]) == [1, 1, 2, 3, 3], 'sorted with duplicates'\n\n# sorted with range\nassert sorted(range(5, 0, -1)) == [1, 2, 3, 4, 5], 'sorted range'\n\ntry:\n    sorted(1, 2)\n    assert False, 'sorted() with too many positional arguments should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('sorted expected 1 argument, got 2',), 'sorted() positional arity error matches CPython'\n\ntry:\n    sorted([1], nope=1)\n    assert False, 'sorted() with invalid keyword should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"sort() got an unexpected keyword argument 'nope'\",), (\n        'sorted() invalid keyword error matches CPython'\n    )\n\n# === sorted() with reverse ===\nassert sorted([3, 1, 2], reverse=True) == [3, 2, 1], 'sorted reverse=True'\nassert sorted([3, 1, 2], reverse=False) == [1, 2, 3], 'sorted reverse=False'\nassert sorted(['c', 'a', 'b'], reverse=True) == ['c', 'b', 'a'], 'sorted strings reverse'\nassert sorted([], reverse=True) == [], 'sorted empty reverse'\nassert sorted([5], reverse=True) == [5], 'sorted single reverse'\nassert sorted([3, 1, 2], reverse=0) == [1, 2, 3], 'sorted reverse=0 (falsy)'\nassert sorted([3, 1, 2], reverse=1) == [3, 2, 1], 'sorted reverse=1 (truthy)'\n\n# === sorted() with key ===\nassert sorted([3, -1, 2, -4], key=abs) == [-1, 2, 3, -4], 'sorted key=abs'\nassert sorted(['banana', 'apple', 'cherry'], key=len) == ['apple', 'banana', 'cherry'], 'sorted key=len'\nassert sorted([3, 1, 2], key=None) == [1, 2, 3], 'sorted key=None same as no key'\n\ntry:\n    sorted([1], key=abs, **{'key': len})\n    assert False, 'duplicate sorted key should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"sorted() got multiple values for keyword argument 'key'\",), (\n        'sorted duplicate key error matches CPython'\n    )\n\n\ndef negate(x):\n    return -x\n\n\nassert sorted([1, -2, 3], key=negate) == [3, 1, -2], 'sorted key=user-defined function'\n\n# === sorted() with key and reverse ===\nassert sorted([3, -1, 2, -4], key=abs, reverse=True) == [-4, 3, 2, -1], 'sorted key=abs reverse=True'\nassert sorted(['banana', 'apple', 'cherry'], key=len, reverse=True) == ['banana', 'cherry', 'apple'], (\n    'sorted key=len reverse=True'\n)\nassert sorted([3, 1, 2], key=None, reverse=True) == [3, 2, 1], 'sorted key=None reverse=True'\n\n# === reversed() ===\n# Basic reversed operations\nassert list(reversed([1, 2, 3])) == [3, 2, 1], 'reversed list'\nassert list(reversed([1])) == [1], 'reversed single element'\nassert list(reversed([])) == [], 'reversed empty list'\n\n# reversed tuple\nassert list(reversed((1, 2, 3))) == [3, 2, 1], 'reversed tuple'\n\n# reversed string\nassert list(reversed('abc')) == ['c', 'b', 'a'], 'reversed string'\n\n# reversed range\nassert list(reversed(range(1, 4))) == [3, 2, 1], 'reversed range'\n\n# === enumerate() ===\n# Basic enumerate operations\nassert list(enumerate(['a', 'b', 'c'])) == [(0, 'a'), (1, 'b'), (2, 'c')], 'enumerate list'\nassert list(enumerate([])) == [], 'enumerate empty list'\nassert list(enumerate(['x'])) == [(0, 'x')], 'enumerate single element'\n\n# enumerate with start\nassert list(enumerate(['a', 'b'], 1)) == [(1, 'a'), (2, 'b')], 'enumerate with start'\nassert list(enumerate(['a', 'b'], 10)) == [(10, 'a'), (11, 'b')], 'enumerate with start 10'\n\n# enumerate string\nassert list(enumerate('ab')) == [(0, 'a'), (1, 'b')], 'enumerate string'\n\n# enumerate range\nassert list(enumerate(range(3))) == [(0, 0), (1, 1), (2, 2)], 'enumerate range'\n\n# === zip() ===\n# Basic zip operations\nassert list(zip([1, 2], ['a', 'b'])) == [(1, 'a'), (2, 'b')], 'zip two lists'\nassert list(zip([1], ['a'])) == [(1, 'a')], 'zip single elements'\nassert list(zip([], [])) == [], 'zip empty lists'\n\n# zip truncates to shortest\nassert list(zip([1, 2, 3], ['a', 'b'])) == [(1, 'a'), (2, 'b')], 'zip truncates to shortest'\nassert list(zip([1], ['a', 'b', 'c'])) == [(1, 'a')], 'zip truncates first shorter'\n\n# zip three iterables\nassert list(zip([1, 2], ['a', 'b'], [True, False])) == [(1, 'a', True), (2, 'b', False)], 'zip three lists'\n\n# zip with different types\nassert list(zip(range(3), 'abc')) == [(0, 'a'), (1, 'b'), (2, 'c')], 'zip range and string'\n\n# zip single iterable\nassert list(zip([1, 2, 3])) == [(1,), (2,), (3,)], 'zip single iterable'\n\n# zip with empty\nassert list(zip([1, 2], [])) == [], 'zip with empty second'\nassert list(zip([], [1, 2])) == [], 'zip with empty first'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__next_stop_iteration.py",
    "content": "it = iter([])\nnext(it)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"builtin__next_stop_iteration.py\", line 2, in <module>\n    next(it)\n    ~~~~~~~~\nStopIteration\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__print_invalid_kwarg.py",
    "content": "print('xxx', **{\"foo'\": 123})\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"builtin__print_invalid_kwarg.py\", line 1, in <module>\n    print('xxx', **{\"foo'\": 123})\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nTypeError: print() got an unexpected keyword argument 'foo''\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__print_kwargs.py",
    "content": "# Tests dynamic keyword arguments for print()\n\n# === Dynamic sep via **kwargs ===\ndynamic_sep = 's' + 'e' + 'p'\nresult = print('left', 'right', **{dynamic_sep: '-'})\nassert result is None, 'print returns None with dynamic sep'\n\n\n# === Dynamic end via **kwargs ===\ndynamic_end = 'e' + 'n' + 'd'\nresult2 = print('line', **{dynamic_end: ''})\nassert result2 is None, 'print returns None with dynamic end'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__repr.py",
    "content": "# === repr of built-in functions ===\nassert repr(len) == '<built-in function len>', 'repr(len)'\nassert repr(print) == '<built-in function print>', 'repr(print)'\n"
  },
  {
    "path": "crates/monty/test_cases/builtin__string_funcs.py",
    "content": "# === ord() ===\n# Basic ord operations\nassert ord('a') == 97, 'ord lowercase a'\nassert ord('A') == 65, 'ord uppercase A'\nassert ord('0') == 48, 'ord digit 0'\nassert ord(' ') == 32, 'ord space'\nassert ord('\\n') == 10, 'ord newline'\nassert ord('\\t') == 9, 'ord tab'\n\n# Unicode characters\nassert ord('\\u00e9') == 233, 'ord e-acute'\nassert ord('\\u4e2d') == 20013, 'ord Chinese character'\nassert ord('\\U0001f600') == 128512, 'ord emoji grinning face'\n\n# === chr() ===\n# Basic chr operations\nassert chr(97) == 'a', 'chr 97 = a'\nassert chr(65) == 'A', 'chr 65 = A'\nassert chr(48) == '0', 'chr 48 = 0'\nassert chr(32) == ' ', 'chr 32 = space'\nassert chr(10) == '\\n', 'chr 10 = newline'\n\n# Unicode characters\nassert chr(233) == '\\u00e9', 'chr 233 = e-acute'\nassert chr(20013) == '\\u4e2d', 'chr 20013 = Chinese char'\nassert chr(128512) == '\\U0001f600', 'chr emoji'\n\n# Edge cases\nassert chr(0) == '\\x00', 'chr 0 = null'\nassert chr(0x10FFFF) != '', 'chr max unicode'\n\n# Round-trip test\nassert chr(ord('x')) == 'x', 'ord/chr roundtrip'\nassert ord(chr(1000)) == 1000, 'chr/ord roundtrip'\n\n# === bin() ===\n# Basic bin operations\nassert bin(0) == '0b0', 'bin 0'\nassert bin(1) == '0b1', 'bin 1'\nassert bin(2) == '0b10', 'bin 2'\nassert bin(5) == '0b101', 'bin 5'\nassert bin(255) == '0b11111111', 'bin 255'\nassert bin(-5) == '-0b101', 'bin negative'\nassert bin(True) == '0b1', 'bin True'\nassert bin(False) == '0b0', 'bin False'\nMIN_I64 = -9223372036854775807 - 1  # Smallest i64\nMIN_I64_BIN = '1' + '0' * 63\nMIN_I64_HEX = '8' + '0' * 15\nMIN_I64_OCT = '1' + '0' * 21\nassert bin(MIN_I64) == '-0b' + MIN_I64_BIN, 'bin handles i64::MIN without overflow'\n\n# === hex() ===\n# Basic hex operations\nassert hex(0) == '0x0', 'hex 0'\nassert hex(15) == '0xf', 'hex 15'\nassert hex(16) == '0x10', 'hex 16'\nassert hex(255) == '0xff', 'hex 255'\nassert hex(256) == '0x100', 'hex 256'\nassert hex(-42) == '-0x2a', 'hex negative'\nassert hex(True) == '0x1', 'hex True'\nassert hex(False) == '0x0', 'hex False'\nassert hex(MIN_I64) == '-0x' + MIN_I64_HEX, 'hex handles i64::MIN without overflow'\n\n# === oct() ===\n# Basic oct operations\nassert oct(0) == '0o0', 'oct 0'\nassert oct(7) == '0o7', 'oct 7'\nassert oct(8) == '0o10', 'oct 8'\nassert oct(64) == '0o100', 'oct 64'\nassert oct(-56) == '-0o70', 'oct negative'\nassert oct(True) == '0o1', 'oct True'\nassert oct(False) == '0o0', 'oct False'\nassert oct(MIN_I64) == '-0o' + MIN_I64_OCT, 'oct handles i64::MIN without overflow'\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__decode_invalid_utf8.py",
    "content": "# Test that bytes.decode raises UnicodeDecodeError for invalid UTF-8\n# UnicodeDecodeError is a subclass of ValueError, so it should be caught by both\n\n# Test it raises UnicodeDecodeError\nraised_decode_error = False\ntry:\n    b'\\xff'.decode()\nexcept UnicodeDecodeError:\n    raised_decode_error = True\nassert raised_decode_error, 'should raise UnicodeDecodeError for invalid UTF-8'\n\n# Test it can be caught by ValueError (since UnicodeDecodeError is a subclass)\ncaught_by_value_error = False\ntry:\n    b'\\x80\\x81'.decode()\nexcept ValueError:\n    caught_by_value_error = True\nassert caught_by_value_error, 'UnicodeDecodeError should be caught by except ValueError'\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__endswith_str_error.py",
    "content": "# Test that bytes.endswith with str raises TypeError\nb'hello'.endswith('o')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"bytes__endswith_str_error.py\", line 2, in <module>\n    b'hello'.endswith('o')\n    ~~~~~~~~~~~~~~~~~~~~~~\nTypeError: endswith first arg must be bytes or a tuple of bytes, not str\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__getitem_index_error.py",
    "content": "b = b'hello'\nb[10]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"bytes__getitem_index_error.py\", line 2, in <module>\n    b[10]\n    ~~~~~\nIndexError: index out of range\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__index_start_gt_end.py",
    "content": "# Test that bytes.index with start > end doesn't panic but raises ValueError\nb'hello'.index(b'e', 5, 2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"bytes__index_start_gt_end.py\", line 2, in <module>\n    b'hello'.index(b'e', 5, 2)\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~\nValueError: subsection not found\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__methods.py",
    "content": "# === bytes.decode() ===\nassert b'hello'.decode() == 'hello', 'decode default utf-8'\nassert b'hello'.decode('utf-8') == 'hello', 'decode explicit utf-8'\nassert b'hello'.decode('utf8') == 'hello', 'decode utf8 variant'\nassert b'hello'.decode('UTF-8') == 'hello', 'decode uppercase UTF-8'\nassert b''.decode() == '', 'decode empty bytes'\n\n# Non-ASCII UTF-8\nassert b'\\xc3\\xa9'.decode() == '\\xe9', 'decode utf-8 e-acute'\nassert b'\\xe4\\xb8\\xad'.decode() == '\\u4e2d', 'decode utf-8 CJK character'\n\n# === bytes.count() ===\nassert b'hello'.count(b'l') == 2, 'count single char'\nassert b'hello'.count(b'll') == 1, 'count subsequence'\nassert b'hello'.count(b'x') == 0, 'count not found'\nassert b'aaa'.count(b'aa') == 1, 'count non-overlapping'\nassert b''.count(b'x') == 0, 'count in empty bytes'\nassert b'hello'.count(b'') == 6, 'count empty subsequence'\n\n# count with start/end\nassert b'abcabc'.count(b'ab', 1) == 1, 'count with start'\nassert b'abcabc'.count(b'ab', 0, 3) == 1, 'count with start and end'\n\n# === bytes.find() ===\nassert b'hello'.find(b'e') == 1, 'find single char'\nassert b'hello'.find(b'll') == 2, 'find subsequence'\nassert b'hello'.find(b'x') == -1, 'find not found'\nassert b'hello'.find(b'') == 0, 'find empty subsequence'\nassert b''.find(b'x') == -1, 'find in empty bytes'\n\n# find with start/end\nassert b'hello'.find(b'l', 3) == 3, 'find with start'\nassert b'hello'.find(b'l', 0, 2) == -1, 'find with end before match'\n\n# === bytes.index() ===\nassert b'hello'.index(b'e') == 1, 'index single char'\nassert b'hello'.index(b'll') == 2, 'index subsequence'\nassert b'hello'.index(b'') == 0, 'index empty subsequence'\n\n# === bytes.startswith() ===\nassert b'hello'.startswith(b'he'), 'startswith true'\nassert not b'hello'.startswith(b'lo'), 'startswith false'\nassert b'hello'.startswith(b''), 'startswith empty'\nassert b''.startswith(b''), 'empty startswith empty'\nassert not b''.startswith(b'x'), 'empty startswith non-empty'\n\n# startswith with start/end\nassert b'abcdef'.startswith(b'bc', 1), 'startswith with start'\nassert b'abcdef'.startswith(b'bc', 1, 3), 'startswith with start and end'\nassert not b'abcdef'.startswith(b'bc', 2), 'startswith with start past match'\nassert not b'abcdef'.startswith(b'abc', 0, 2), 'startswith with end before match ends'\n\n# === bytes.endswith() ===\nassert b'hello'.endswith(b'lo'), 'endswith true'\nassert not b'hello'.endswith(b'he'), 'endswith false'\nassert b'hello'.endswith(b''), 'endswith empty'\nassert b''.endswith(b''), 'empty endswith empty'\nassert not b''.endswith(b'x'), 'empty endswith non-empty'\n\n# endswith with start/end\nassert b'abcdef'.endswith(b'de', 0, 5), 'endswith with end'\nassert b'abcdef'.endswith(b'cd', 1, 4), 'endswith with start and end'\nassert not b'abcdef'.endswith(b'de', 0, 4), 'endswith before suffix'\n\n# === Edge case: start > end (should not panic, treat as empty slice) ===\nassert b'hello'.find(b'e', 5, 2) == -1, 'find with start > end returns -1'\nassert b'hello'.count(b'l', 5, 2) == 0, 'count with start > end returns 0'\nassert not b'hello'.startswith(b'h', 5, 2), 'startswith with start > end is false'\nassert not b'hello'.endswith(b'o', 5, 2), 'endswith with start > end is false'\n\n# === bytes.lower() ===\nassert b'HELLO'.lower() == b'hello', 'lower basic'\nassert b'Hello World'.lower() == b'hello world', 'lower mixed case'\nassert b'hello'.lower() == b'hello', 'lower already lowercase'\nassert b''.lower() == b'', 'lower empty'\nassert b'123ABC'.lower() == b'123abc', 'lower with digits'\nassert b'\\x80\\xff'.lower() == b'\\x80\\xff', 'lower non-ascii unchanged'\n\n# === bytes.upper() ===\nassert b'hello'.upper() == b'HELLO', 'upper basic'\nassert b'Hello World'.upper() == b'HELLO WORLD', 'upper mixed case'\nassert b'HELLO'.upper() == b'HELLO', 'upper already uppercase'\nassert b''.upper() == b'', 'upper empty'\nassert b'123abc'.upper() == b'123ABC', 'upper with digits'\n\n# === bytes.capitalize() ===\nassert b'hello'.capitalize() == b'Hello', 'capitalize basic'\nassert b'HELLO'.capitalize() == b'Hello', 'capitalize uppercase'\nassert b'hELLO wORLD'.capitalize() == b'Hello world', 'capitalize mixed'\nassert b''.capitalize() == b'', 'capitalize empty'\nassert b'123hello'.capitalize() == b'123hello', 'capitalize starting with digit'\n\n# === bytes.title() ===\nassert b'hello world'.title() == b'Hello World', 'title basic'\nassert b'HELLO WORLD'.title() == b'Hello World', 'title uppercase'\nassert b\"they're bill's\".title() == b\"They'Re Bill'S\", 'title with apostrophe'\nassert b''.title() == b'', 'title empty'\n\n# === bytes.swapcase() ===\nassert b'Hello World'.swapcase() == b'hELLO wORLD', 'swapcase basic'\nassert b'HELLO'.swapcase() == b'hello', 'swapcase uppercase'\nassert b'hello'.swapcase() == b'HELLO', 'swapcase lowercase'\nassert b''.swapcase() == b'', 'swapcase empty'\n\n# === bytes.isalpha() ===\nassert b'hello'.isalpha(), 'isalpha all letters'\nassert not b'hello123'.isalpha(), 'isalpha with digits'\nassert not b'hello world'.isalpha(), 'isalpha with space'\nassert not b''.isalpha(), 'isalpha empty is false'\nassert b'ABC'.isalpha(), 'isalpha uppercase'\n\n# === bytes.isdigit() ===\nassert b'123'.isdigit(), 'isdigit all digits'\nassert not b'123abc'.isdigit(), 'isdigit with letters'\nassert not b''.isdigit(), 'isdigit empty is false'\n\n# === bytes.isalnum() ===\nassert b'hello123'.isalnum(), 'isalnum letters and digits'\nassert b'hello'.isalnum(), 'isalnum all letters'\nassert b'123'.isalnum(), 'isalnum all digits'\nassert not b'hello world'.isalnum(), 'isalnum with space'\nassert not b''.isalnum(), 'isalnum empty is false'\n\n# === bytes.isspace() ===\nassert b' \\t\\n\\r'.isspace(), 'isspace whitespace chars'\nassert not b'hello'.isspace(), 'isspace not all whitespace'\nassert not b''.isspace(), 'isspace empty is false'\nassert b' '.isspace(), 'isspace single space'\n\n# === bytes.islower() ===\nassert b'hello'.islower(), 'islower all lowercase'\nassert b'hello123'.islower(), 'islower with digits'\nassert not b'Hello'.islower(), 'islower with uppercase'\nassert not b'HELLO'.islower(), 'islower all uppercase'\nassert not b''.islower(), 'islower empty is false'\nassert not b'123'.islower(), 'islower no cased chars is false'\n\n# === bytes.isupper() ===\nassert b'HELLO'.isupper(), 'isupper all uppercase'\nassert b'HELLO123'.isupper(), 'isupper with digits'\nassert not b'Hello'.isupper(), 'isupper with lowercase'\nassert not b'hello'.isupper(), 'isupper all lowercase'\nassert not b''.isupper(), 'isupper empty is false'\nassert not b'123'.isupper(), 'isupper no cased chars is false'\n\n# === bytes.isascii() ===\nassert b'hello'.isascii(), 'isascii all ascii'\nassert b''.isascii(), 'isascii empty is true'\nassert b'\\x00\\x7f'.isascii(), 'isascii boundary values'\nassert not b'\\x80'.isascii(), 'isascii non-ascii byte'\nassert not b'hello\\xff'.isascii(), 'isascii with non-ascii'\n\n# === bytes.istitle() ===\nassert b'Hello World'.istitle(), 'istitle basic'\nassert not b'hello world'.istitle(), 'istitle lowercase'\nassert not b'HELLO WORLD'.istitle(), 'istitle uppercase'\nassert b'Hello'.istitle(), 'istitle single word'\nassert not b''.istitle(), 'istitle empty is false'\n\n# === bytes.rfind() ===\nassert b'hello'.rfind(b'l') == 3, 'rfind finds last occurrence'\nassert b'hello'.rfind(b'x') == -1, 'rfind not found'\nassert b'hello'.rfind(b'') == 5, 'rfind empty at end'\nassert b'aaaa'.rfind(b'aa') == 2, 'rfind non-overlapping from right'\nassert b'hello'.rfind(b'l', 0, 3) == 2, 'rfind with range'\n\n# === bytes.rindex() ===\nassert b'hello'.rindex(b'l') == 3, 'rindex finds last occurrence'\nassert b'hello'.rindex(b'') == 5, 'rindex empty at end'\n\n# === bytes.strip() ===\nassert b'  hello  '.strip() == b'hello', 'strip whitespace'\nassert b'hello'.strip() == b'hello', 'strip no whitespace'\nassert b'xxxhelloxxx'.strip(b'x') == b'hello', 'strip custom chars'\nassert b''.strip() == b'', 'strip empty'\nassert b'   '.strip() == b'', 'strip all whitespace'\n\n# === bytes.lstrip() ===\nassert b'  hello  '.lstrip() == b'hello  ', 'lstrip whitespace'\nassert b'xxxhello'.lstrip(b'x') == b'hello', 'lstrip custom chars'\nassert b''.lstrip() == b'', 'lstrip empty'\n\n# === bytes.rstrip() ===\nassert b'  hello  '.rstrip() == b'  hello', 'rstrip whitespace'\nassert b'helloxxx'.rstrip(b'x') == b'hello', 'rstrip custom chars'\nassert b''.rstrip() == b'', 'rstrip empty'\n\n# === bytes.removeprefix() ===\nassert b'hello'.removeprefix(b'he') == b'llo', 'removeprefix found'\nassert b'hello'.removeprefix(b'xxx') == b'hello', 'removeprefix not found'\nassert b'hello'.removeprefix(b'') == b'hello', 'removeprefix empty'\nassert b''.removeprefix(b'x') == b'', 'removeprefix empty bytes'\n\n# === bytes.removesuffix() ===\nassert b'hello'.removesuffix(b'lo') == b'hel', 'removesuffix found'\nassert b'hello'.removesuffix(b'xxx') == b'hello', 'removesuffix not found'\nassert b'hello'.removesuffix(b'') == b'hello', 'removesuffix empty'\nassert b''.removesuffix(b'x') == b'', 'removesuffix empty bytes'\n\n# === bytes.split() ===\nassert b'a,b,c'.split(b',') == [b'a', b'b', b'c'], 'split basic'\nassert b'a b c'.split() == [b'a', b'b', b'c'], 'split whitespace'\nassert b'a  b  c'.split() == [b'a', b'b', b'c'], 'split multiple whitespace'\nassert b'a,b,c'.split(b',', 1) == [b'a', b'b,c'], 'split maxsplit'\nassert b''.split() == [], 'split empty bytes'\nassert b'hello'.split(b'x') == [b'hello'], 'split not found'\n\n# === bytes.rsplit() ===\nassert b'a,b,c'.rsplit(b',') == [b'a', b'b', b'c'], 'rsplit basic'\nassert b'a,b,c'.rsplit(b',', 1) == [b'a,b', b'c'], 'rsplit maxsplit'\nassert b'a b c'.rsplit() == [b'a', b'b', b'c'], 'rsplit whitespace'\n\n# === bytes.splitlines() ===\nassert b'a\\nb\\nc'.splitlines() == [b'a', b'b', b'c'], 'splitlines newline'\nassert b'a\\r\\nb\\rc'.splitlines() == [b'a', b'b', b'c'], 'splitlines mixed'\nassert b'a\\nb\\n'.splitlines() == [b'a', b'b'], 'splitlines trailing'\nassert b'a\\nb'.splitlines(True) == [b'a\\n', b'b'], 'splitlines keepends'\nassert b''.splitlines() == [], 'splitlines empty'\n\n# === bytes.partition() ===\nassert b'hello world'.partition(b' ') == (b'hello', b' ', b'world'), 'partition found'\nassert b'hello'.partition(b'x') == (b'hello', b'', b''), 'partition not found'\nassert b'hello world here'.partition(b' ') == (b'hello', b' ', b'world here'), 'partition first'\n\n# === bytes.rpartition() ===\nassert b'hello world'.rpartition(b' ') == (b'hello', b' ', b'world'), 'rpartition found'\nassert b'hello'.rpartition(b'x') == (b'', b'', b'hello'), 'rpartition not found'\nassert b'hello world here'.rpartition(b' ') == (b'hello world', b' ', b'here'), 'rpartition last'\n\n# === bytes.replace() ===\nassert b'hello'.replace(b'l', b'L') == b'heLLo', 'replace all'\nassert b'hello'.replace(b'l', b'L', 1) == b'heLlo', 'replace count'\nassert b'hello'.replace(b'x', b'y') == b'hello', 'replace not found'\nassert b'aaa'.replace(b'a', b'bb') == b'bbbbbb', 'replace longer'\nassert b'aaa'.replace(b'aa', b'b') == b'ba', 'replace non-overlapping'\n\n# === bytes.center() ===\nassert b'hello'.center(10) == b'  hello   ', 'center basic'\nassert b'hello'.center(10, b'*') == b'**hello***', 'center fillbyte'\nassert b'hello'.center(3) == b'hello', 'center too short'\n\n# === bytes.ljust() ===\nassert b'hello'.ljust(10) == b'hello     ', 'ljust basic'\nassert b'hello'.ljust(10, b'*') == b'hello*****', 'ljust fillbyte'\nassert b'hello'.ljust(3) == b'hello', 'ljust too short'\n\n# === bytes.rjust() ===\nassert b'hello'.rjust(10) == b'     hello', 'rjust basic'\nassert b'hello'.rjust(10, b'*') == b'*****hello', 'rjust fillbyte'\nassert b'hello'.rjust(3) == b'hello', 'rjust too short'\n\n# === bytes.zfill() ===\nassert b'42'.zfill(5) == b'00042', 'zfill basic'\nassert b'-42'.zfill(5) == b'-0042', 'zfill negative'\nassert b'+42'.zfill(5) == b'+0042', 'zfill positive'\nassert b'hello'.zfill(3) == b'hello', 'zfill too short'\n\n# === bytes.join() ===\nassert b','.join([b'a', b'b', b'c']) == b'a,b,c', 'join list'\nassert b''.join([b'a', b'b']) == b'ab', 'join empty separator'\nassert b','.join([]) == b'', 'join empty iterable'\nassert b'-'.join([b'hello']) == b'hello', 'join single item'\n\n# === bytes.hex() ===\nassert b'\\xde\\xad\\xbe\\xef'.hex() == 'deadbeef', 'hex basic'\nassert b''.hex() == '', 'hex empty'\nassert b'AB'.hex() == '4142', 'hex letters'\nassert b'\\x00\\xff'.hex() == '00ff', 'hex boundary'\nassert b'\\xde\\xad\\xbe\\xef'.hex(':') == 'de:ad:be:ef', 'hex with separator'\nassert b'\\xde\\xad\\xbe\\xef'.hex(':', 2) == 'dead:beef', 'hex with bytes_per_sep'\n# Test positive bytes_per_sep (partial group at start)\nassert b'\\x01\\x02\\x03\\x04\\x05'.hex(':', 2) == '01:0203:0405', 'hex +2 odd bytes'\nassert b'\\x01\\x02\\x03'.hex(':', 2) == '01:0203', 'hex +2 three bytes'\n# Test negative bytes_per_sep (partial group at end)\nassert b'\\x01\\x02\\x03\\x04\\x05'.hex(':', -2) == '0102:0304:05', 'hex -2 odd bytes'\nassert b'\\x01\\x02\\x03'.hex(':', -2) == '0102:03', 'hex -2 three bytes'\n\n# === bytes.fromhex() ===\nassert bytes.fromhex('deadbeef') == b'\\xde\\xad\\xbe\\xef', 'fromhex basic'\nassert bytes.fromhex('DEADBEEF') == b'\\xde\\xad\\xbe\\xef', 'fromhex uppercase'\nassert bytes.fromhex('') == b'', 'fromhex empty'\nassert bytes.fromhex('de ad be ef') == b'\\xde\\xad\\xbe\\xef', 'fromhex with spaces'\nassert bytes.fromhex('4142') == b'AB', 'fromhex letters'\n\n# === bytes.fromhex() with whitespace ===\n# Whitespace is only allowed BETWEEN byte pairs, not within a pair\nassert bytes.fromhex(' 01 ') == b'\\x01', 'fromhex whitespace around bytes is stripped'\nassert bytes.fromhex('01 23') == b'\\x01\\x23', 'fromhex whitespace between byte pairs'\n\n# === bytes.fromhex() errors ===\n# Odd number of hex digits (no invalid chars, just odd count)\ntry:\n    bytes.fromhex('0')\n    assert False, 'fromhex odd digits should error'\nexcept ValueError as e:\n    assert str(e) == 'fromhex() arg must contain an even number of hexadecimal digits', (\n        f'fromhex odd digits message, error: {e}'\n    )\n\ntry:\n    bytes.fromhex(' 0')\n    assert False, 'fromhex odd digits after whitespace should error'\nexcept ValueError as e:\n    assert str(e) == 'fromhex() arg must contain an even number of hexadecimal digits', (\n        f'fromhex odd digits after whitespace message, error: {e}'\n    )\n\n# Whitespace within a byte pair is invalid (space is not a hex digit)\ntry:\n    bytes.fromhex('0 1')\n    assert False, 'fromhex whitespace within pair should error'\nexcept ValueError as e:\n    assert str(e) == 'non-hexadecimal number found in fromhex() arg at position 1', (\n        f'fromhex whitespace within pair message, error: {e}'\n    )\n\n# Invalid hex character\ntry:\n    bytes.fromhex('0g')\n    assert False, 'fromhex invalid hex char should error'\nexcept ValueError as e:\n    assert str(e) == 'non-hexadecimal number found in fromhex() arg at position 1', (\n        f'fromhex invalid hex char message, error: {e}'\n    )\n\n# === bytes.fromhex() instance access ===\n# fromhex is a classmethod but should also work on instances\nassert b''.fromhex('4142') == b'AB', 'fromhex on bytes instance'\nassert b'hello'.fromhex('deadbeef') == b'\\xde\\xad\\xbe\\xef', 'fromhex on non-empty instance'\n\n# === bytes.startswith/endswith with tuple of prefixes ===\nassert b'hello'.startswith((b'he', b'wo')), 'startswith tuple first match'\nassert b'hello'.startswith((b'wo', b'he')), 'startswith tuple second match'\nassert not b'hello'.startswith((b'wo', b'ab')), 'startswith tuple no match'\nassert b'hello'.startswith((b'',)), 'startswith tuple with empty bytes'\nassert b'hello'.startswith((b'hello', b'world')), 'startswith tuple exact match'\n\nassert b'hello'.endswith((b'lo', b'ld')), 'endswith tuple first match'\nassert b'hello'.endswith((b'ld', b'lo')), 'endswith tuple second match'\nassert not b'hello'.endswith((b'he', b'ab')), 'endswith tuple no match'\nassert b'hello'.endswith((b'',)), 'endswith tuple with empty bytes'\nassert b'hello'.endswith((b'hello', b'world')), 'endswith tuple exact match'\n\n# startswith/endswith tuple with start/end\nassert b'abcdef'.startswith((b'bc', b'cd'), 1), 'startswith tuple with start'\nassert b'abcdef'.endswith((b'de', b'cd'), 0, 5), 'endswith tuple with end'\n\n# === Empty-substring edge cases ===\n# Edge case: start == len (boundary) - this works\nassert b'hello'.find(b'', 5) == 5, 'find empty at len returns len'\nassert b'hello'.count(b'', 5) == 1, 'count empty at len returns 1'\nassert b'hello'.startswith(b'', 5), 'startswith empty at len is true'\nassert b'hello'.endswith(b'', 5), 'endswith empty at len is true'\n\n# TODO: These edge cases when start > len need to be fixed\n# CPython returns -1/0/False for these, currently Monty doesn't handle this correctly\n# assert b'hello'.find(b'', 10) == -1, 'find empty when start > len returns -1'\n# assert b'hello'.count(b'', 10) == 0, 'count empty when start > len returns 0'\n# assert not b'hello'.startswith(b'', 10), 'startswith empty when start > len is false'\n# assert not b'hello'.endswith(b'', 10), 'endswith empty when start > len is false'\n# assert b'hello'.rfind(b'', 10) == -1, 'rfind empty when start > len returns -1'\n\n# === bytes.hex() non-ASCII separator errors ===\ntry:\n    b'\\x01\\x02'.hex('\\xff')\n    assert False, 'hex with non-ASCII separator should error'\nexcept ValueError as e:\n    # CPython uses 'sep must be ASCII.' with period\n    msg = str(e)\n    assert 'sep' in msg.lower() and 'ascii' in msg.lower(), f'hex non-ASCII sep message, error: {e}'\n\n# === bytes.decode() with errors argument ===\n# Valid errors values\nassert b'hello'.decode('utf-8', 'strict') == 'hello', 'decode with strict errors'\nassert b'hello'.decode('utf-8', 'ignore') == 'hello', 'decode with ignore errors'\nassert b'hello'.decode('utf-8', 'replace') == 'hello', 'decode with replace errors'\n\n# TODO: errors argument type validation - CPython raises TypeError for non-string errors\n# This is not implemented yet\n# try:\n#     b'hello'.decode('utf-8', 123)\n#     assert False, 'decode with non-string errors should error'\n# except TypeError as e:\n#     assert 'str' in str(e), f'decode errors type error should mention str, error: {e}'\n\n# === Error message for unknown classmethod ===\n# Error message should say 'bytes' not 'type'\ntry:\n    bytes.nonexistent()\n    assert False, 'should raise AttributeError'\nexcept AttributeError as e:\n    msg = str(e)\n    assert 'bytes' in msg, f'error should mention bytes, got: {e}'\n    assert 'nonexistent' in msg, f'error should mention method name, got: {e}'\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__negative_count.py",
    "content": "bytes(-1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"bytes__negative_count.py\", line 1, in <module>\n    bytes(-1)\n    ~~~~~~~~~\nValueError: negative count\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__ops.py",
    "content": "# === Bytes length ===\nassert len(b'') == 0, 'len empty'\nassert len(b'hello') == 5, 'len basic'\n\n# === Bytes repr/str ===\nassert repr(b'hello') == \"b'hello'\", 'bytes repr'\nassert str(b'hello') == \"b'hello'\", 'bytes str'\n\n# === Various bytes repr cases ===\nassert repr(b'') == \"b''\", 'empty bytes repr'\nassert repr(b\"it's\") == 'b\"it\\'s\"', 'single quote bytes repr'\nassert repr(b'l1\\nl2') == \"b'l1\\\\nl2'\", 'newline bytes repr'\nassert repr(b'col1\\tcol2') == \"b'col1\\\\tcol2'\", 'tab bytes repr'\nassert repr(b'\\x00\\xff') == \"b'\\\\x00\\\\xff'\", 'non-printable bytes repr'\nassert repr(b'back\\\\slash') == \"b'back\\\\\\\\slash'\", 'backslash bytes repr'\n\n# === Bytes repetition (*) ===\nassert b'ab' * 3 == b'ababab', 'bytes mult int'\nassert 3 * b'ab' == b'ababab', 'int mult bytes'\nassert b'x' * 0 == b'', 'bytes mult zero'\nassert b'x' * -1 == b'', 'bytes mult negative'\nassert b'' * 5 == b'', 'empty bytes mult'\nassert b'ab' * 1 == b'ab', 'bytes mult one'\n\n# === Bytes indexing (getitem) ===\n# Basic indexing - returns integer byte values\nassert b'hello'[0] == 104, 'bytes getitem index 0 (h=104)'\nassert b'hello'[1] == 101, 'bytes getitem index 1 (e=101)'\nassert b'hello'[4] == 111, 'bytes getitem last index (o=111)'\n\n# Negative indexing\nassert b'hello'[-1] == 111, 'bytes getitem -1 (o=111)'\nassert b'hello'[-2] == 108, 'bytes getitem -2 (l=108)'\nassert b'hello'[-5] == 104, 'bytes getitem -5 (h=104)'\n\n# Single byte\nassert b'x'[0] == 120, 'bytes getitem single byte at 0'\nassert b'x'[-1] == 120, 'bytes getitem single byte at -1'\n\n# ASCII printable range\nassert b' '[0] == 32, 'bytes getitem space (32)'\nassert b'~'[0] == 126, 'bytes getitem tilde (126)'\n\n# Non-printable bytes\nassert b'\\x00'[0] == 0, 'bytes getitem null byte'\nassert b'\\xff'[0] == 255, 'bytes getitem 0xff'\nassert b'\\n'[0] == 10, 'bytes getitem newline'\nassert b'\\t'[0] == 9, 'bytes getitem tab'\n\n# Heap-allocated bytes\nb = bytes(b'abc')\nassert b[0] == 97, 'heap bytes getitem 0'\nassert b[1] == 98, 'heap bytes getitem 1'\nassert b[-1] == 99, 'heap bytes negative getitem'\n\n# Variable index\nb = b'xyz'\ni = 1\nassert b[i] == 121, 'bytes getitem with variable index'\n\n# Verify return type is int\nval = b'A'[0]\nassert type(val) == int, 'bytes getitem returns int'\nassert val == 65, 'bytes getitem value is correct'\n\n# Bool indices (True=1, False=0)\nb = b'abc'\nassert b[False] == 97, 'bytes getitem with False'\nassert b[True] == 98, 'bytes getitem with True'\n\n# === Bytes comparisons ===\nassert b'abc' < b'abd', 'bytes < bytes'\nassert b'abd' > b'abc', 'bytes > bytes'\nassert b'abc' <= b'abc', 'bytes <= bytes equal'\nassert b'abc' <= b'abd', 'bytes <= bytes less'\nassert b'abd' >= b'abd', 'bytes >= bytes equal'\nassert b'abd' >= b'abc', 'bytes >= bytes greater'\n\n# Different lengths\nassert b'ab' < b'abc', 'shorter prefix is less'\nassert b'' < b'a', 'empty bytes is less'\nassert b'abc' > b'ab', 'longer bytes with same prefix is greater'\n\n# Non-ASCII byte values\nassert b'\\x00' < b'\\xff', 'null byte < 0xff'\nassert b'\\xfe' < b'\\xff', '0xfe < 0xff'\n\n# Sorting\nassert sorted([b'c', b'a', b'b']) == [b'a', b'b', b'c'], 'sorted bytes list'\nassert sorted([b'bb', b'a', b'ba']) == [b'a', b'ba', b'bb'], 'sorted different length bytes'\n"
  },
  {
    "path": "crates/monty/test_cases/bytes__startswith_str_error.py",
    "content": "# Test that bytes.startswith with str raises TypeError\nb'hello'.startswith('h')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"bytes__startswith_str_error.py\", line 2, in <module>\n    b'hello'.startswith('h')\n    ~~~~~~~~~~~~~~~~~~~~~~~~\nTypeError: startswith first arg must be bytes or a tuple of bytes, not str\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/call_object.py",
    "content": "x = len\nx('abc')\n# Return=3\n"
  },
  {
    "path": "crates/monty/test_cases/chain_comparison__all.py",
    "content": "# === Basic chain comparisons ===\nassert (1 < 2 < 3) == True, 'ascending chain'\nassert (1 < 3 < 2) == False, 'fails at second comparison'\nassert (3 < 2 < 1) == False, 'fails at first comparison'\nassert (1 <= 2 <= 2) == True, 'with equality'\nassert 1 <= 2 <= 2, 'with equality'\nassert 1 <= 2 <= 2 <= 3, 'chained with equality'\n\n# === Mixed operators ===\nassert (1 < 2 <= 2 < 3) == True, 'mixed lt and le'\nassert (1 == 1 == 1) == True, 'triple equality'\nassert (1 != 2 != 1) == True, 'not-equal chain (not transitive)'\n\n# === Longer chains ===\nassert (1 < 2 < 3 < 4 < 5) == True, '5-way ascending'\nassert (1 < 2 < 3 < 2 < 5) == False, 'fails in middle'\n\n# === With variables and expressions ===\nx = 5\nassert (1 < x < 10) == True, 'variable in chain'\nassert (0 < x - 3 < x < x + 1) == True, 'expressions'\n\n\n# === Short-circuit evaluation ===\ndef test_short_circuit():\n    calls = []\n\n    def a():\n        calls.append('a')\n        return 1\n\n    def b():\n        calls.append('b')\n        return 0  # This will make first comparison fail\n\n    def c():\n        calls.append('c')\n        return 2\n\n    # Test: first comparison fails, c() should not be called\n    result = a() < b() < c()  # 1 < 0 is False, c() should not be called\n    assert result == False, 'short circuit result'\n    assert calls == ['a', 'b'], 'c not called due to short circuit'\n\n\ntest_short_circuit()\n\n\n# === Single evaluation of intermediate values ===\ndef test_single_eval():\n    count = 0\n\n    def middle():\n        nonlocal count\n        count += 1\n        return 5\n\n    result = 1 < middle() < 10\n    assert result == True, 'chain result'\n    assert count == 1, 'middle() called exactly once'\n\n\ntest_single_eval()\n\n# === Identity comparisons ===\na = [1]\nb = a\nc = a\nassert (a is b is c) == True, 'is chain same object'\n\n# === Containment checks ===\nassert (1 in [1, 2] in [[1, 2], [3]]) == True, 'in chain'\n\n\n# === Verify no namespace pollution ===\n# Note: The old implementation used _chain_cmp_N variables which would leak.\n# The new stack-based implementation doesn't create any intermediate variables.\n# We can't easily test for namespace pollution without dir(), so we just verify\n# that chain comparisons work correctly (covered by tests above).\n"
  },
  {
    "path": "crates/monty/test_cases/closure__param_shadows_outer.py",
    "content": "# === Parameter shadows outer local (basic) ===\ndef outer_basic():\n    x = 42\n\n    def inner(x):\n        return x + 1\n\n    return inner(10)\n\n\nassert outer_basic() == 11, 'inner param should shadow outer local'\n\n\n# === Parameter shadows outer local (multiple params) ===\ndef outer_multi():\n    a = 100\n    b = 200\n\n    def inner(a, b):\n        return a + b\n\n    return inner(1, 2)\n\n\nassert outer_multi() == 3, 'both params should shadow outer locals'\n\n\n# === Mixed: one param shadows, one captures ===\ndef outer_mixed():\n    x = 10\n    y = 20\n\n    def inner(x):\n        return x + y\n\n    return inner(5)\n\n\nassert outer_mixed() == 25, 'x should be param (5), y should be captured (20)'\n\n\n# === Parameter shadows with default value ===\ndef outer_default():\n    x = 99\n\n    def inner(x=7):\n        return x\n\n    return inner()\n\n\nassert outer_default() == 7, 'default param should shadow outer local'\n\n\n# === Deeply nested: param shadows grandparent local ===\ndef outer_deep():\n    x = 1000\n\n    def middle():\n        def inner(x):\n            return x * 2\n\n        return inner(3)\n\n    return middle()\n\n\nassert outer_deep() == 6, 'inner param should shadow grandparent local'\n\n\n# === Parameter used in complex expression ===\ndef outer_expr():\n    scale = 100\n\n    def inner(n, scale):\n        return n * scale + 1\n\n    return inner(5, 10)\n\n\nassert outer_expr() == 51, 'scale param should shadow outer scale'\n"
  },
  {
    "path": "crates/monty/test_cases/closure__pep448.py",
    "content": "# Tests for PEP 448 unpacking inside closures.\n# These exercise the collect_*_from_expr helpers in prepare.rs which walk\n# expressions to find walrus-operator assignments, cell variables, and\n# referenced names in nested functions. Closures that reference variables\n# used in PEP 448 positions (dict unpack, list/tuple/set literal, call args)\n# are the only way to reach these code paths.\n\n# === Closure capturing variable used in dict unpack ===\ndef outer_dict():\n    d1 = {'a': 1, 'b': 2}\n    d2 = {'c': 3}\n\n    def inner():\n        return {**d1, **d2}\n\n    return inner()\n\n\nassert outer_dict() == {'a': 1, 'b': 2, 'c': 3}, 'closure: dict unpack'\n\n\n# === Closure capturing variable used in list unpack ===\ndef outer_list():\n    items = [1, 2, 3]\n    extra = [4, 5]\n\n    def inner():\n        return [*items, *extra]\n\n    return inner()\n\n\nassert outer_list() == [1, 2, 3, 4, 5], 'closure: list unpack'\n\n\n# === Closure capturing variable used in tuple unpack ===\ndef outer_tuple():\n    a = (1, 2)\n    b = (3, 4)\n\n    def inner():\n        return (*a, *b)\n\n    return inner()\n\n\nassert outer_tuple() == (1, 2, 3, 4), 'closure: tuple unpack'\n\n\n# === Closure capturing variable used in set unpack ===\ndef outer_set():\n    items = [1, 2, 3]\n\n    def inner():\n        return {*items}\n\n    return inner()\n\n\nassert outer_set() == {1, 2, 3}, 'closure: set unpack'\n\n\n# === Closure using PEP 448 in a function call (single * and **) ===\ndef outer_call_star():\n    def f(*args, **kwargs):\n        return (args, kwargs)\n\n    args = [1, 2, 3]\n    kw = {'x': 10}\n\n    def inner():\n        return f(*args, **kw)\n\n    return inner()\n\n\nassert outer_call_star() == ((1, 2, 3), {'x': 10}), 'closure: call *args **kw'\n\n\n# === Closure using multiple * and ** in a call ===\ndef outer_multi():\n    def f(*args, **kwargs):\n        return (args, kwargs)\n\n    a = [1, 2]\n    b = [3, 4]\n    kw1 = {'x': 10}\n    kw2 = {'y': 20}\n\n    def inner():\n        return f(*a, *b, **kw1, **kw2)\n\n    return inner()\n\n\nassert outer_multi() == ((1, 2, 3, 4), {'x': 10, 'y': 20}), 'closure: multi-star call'\n\n\n# === Closure calling with keyword-only args (ArgExprs::Kwargs) ===\n# The outer-function assignment `_precomp = f(a=val_a, b=val_b)` exercises\n# collect_assigned_names_from_args for the Kwargs arm.  The inner body exercises\n# collect_cell_vars_from_args and collect_referenced_names_from_args for Kwargs.\ndef outer_kwargs_call():\n    def f(**kwargs):\n        return kwargs\n\n    val_a = 10\n    val_b = 20\n    # Assignment RHS is a Kwargs call → triggers collect_assigned_names_from_args Kwargs arm\n    _precomp = f(a=val_a, b=val_b)\n\n    def inner():\n        # Kwargs call in inner body → collect_cell/referenced_names Kwargs arm\n        return f(a=val_a, b=val_b)\n\n    return inner()\n\n\nassert outer_kwargs_call() == {'a': 10, 'b': 20}, 'closure: keyword-only call'\n\n\n# === Closure calling with positional + *star (ArgsKargs with args=Some) ===\n# Exercises ArgsKargs branch where the positional-args field is non-None.\ndef outer_argsstar():\n    def f(*args):\n        return list(args)\n\n    pos_val = 1\n    items = [2, 3]\n    # Assignment RHS has positional arg + star → ArgsKargs with args=Some([pos_val])\n    _precomp = f(pos_val, *items)\n\n    def inner():\n        return f(pos_val, *items)\n\n    return inner()\n\n\nassert outer_argsstar() == [1, 2, 3], 'closure: positional + *args'\n\n\n# === Closure calling with named kwarg + **kw (ArgsKargs with kwargs=Some) ===\n# Exercises ArgsKargs branch where the named-kwargs field is non-None.\ndef outer_kwargsstar():\n    def f(**kwargs):\n        return kwargs\n\n    val = 1\n    extra = {'b': 2}\n    # Assignment RHS has named kwarg + double-star → ArgsKargs with kwargs=Some\n    _precomp = f(a=val, **extra)\n\n    def inner():\n        return f(a=val, **extra)\n\n    return inner()\n\n\nassert outer_kwargsstar() == {'a': 1, 'b': 2}, 'closure: named kwarg + **kw'\n\n\n# === Closure with GeneralizedCall and Named kwarg ===\n# Exercises the CallKwarg::Named arm in all three collect_*_names_from_args functions.\ndef outer_generalized_named():\n    def f(*args, **kwargs):\n        return (args, kwargs)\n\n    a = [1, 2]\n    b = [3]\n    key_val = 99\n    # Assignment RHS has GeneralizedCall with Named kwarg → collect_assigned_names Named arm\n    _precomp = f(*a, *b, key=key_val)\n\n    def inner():\n        # Named kwarg in GeneralizedCall → collect_cell/referenced_names Named arm\n        return f(*a, *b, key=key_val)\n\n    return inner()\n\n\nassert outer_generalized_named() == ((1, 2, 3), {'key': 99}), 'closure: generalized call with named kwarg'\n\n\n# === Closure with GeneralizedCall and plain Value arg ===\n# Exercises the CallArg::Value path of the GeneralizedCall args loop,\n# covering the Value branch of the `CallArg::Value | CallArg::Unpack` OR-pattern.\ndef outer_generalized_mixed():\n    def f(*args):\n        return list(args)\n\n    const = 0\n    items1 = [1, 2]\n    items2 = [3, 4]\n    # GeneralizedCall with Value(const) + Unpack(items1) + Unpack(items2)\n    _precomp = f(const, *items1, *items2)\n\n    def inner():\n        return f(const, *items1, *items2)\n\n    return inner()\n\n\nassert outer_generalized_mixed() == [0, 1, 2, 3, 4], 'closure: generalized call with value + unpack args'\n"
  },
  {
    "path": "crates/monty/test_cases/closure__undefined_nonlocal.py",
    "content": "# accessing nonlocal before assignment should raise NameError\ndef outer():\n    def inner():\n        nonlocal x\n        return x  # x not yet defined\n\n    result = inner()\n    x = 10\n    return result\n\n\nouter()\n# Raise=NameError(\"cannot access free variable 'x' where it is not associated with a value in enclosing scope\")\n"
  },
  {
    "path": "crates/monty/test_cases/compare__mixed_types.py",
    "content": "# === Bool == Int equality ===\nassert True == 1, 'True == 1'\nassert False == 0, 'False == 0'\nassert 1 == True, '1 == True'\nassert 0 == False, '0 == False'\nassert True != 2, 'True != 2'\nassert False != 1, 'False != 1'\n\n# === Bool == Float equality ===\nassert True == 1.0, 'True == 1.0'\nassert False == 0.0, 'False == 0.0'\nassert 1.0 == True, '1.0 == True'\nassert 0.0 == False, '0.0 == False'\nassert True != 2.0, 'True != 2.0'\nassert 0.5 != False, '0.5 != False'\n\n# === Int == Float equality ===\nassert 5 == 5.0, '5 == 5.0'\nassert 5.0 == 5, '5.0 == 5'\nassert 5 != 5.5, '5 != 5.5'\nassert 0 == 0.0, '0 == 0.0'\nassert -3 == -3.0, '-3 == -3.0'\n\n# === Int/Float ordering ===\nassert 5 < 5.5, '5 < 5.5'\nassert 5.5 > 5, '5.5 > 5'\nassert 5 <= 5.0, '5 <= 5.0'\nassert 5.0 >= 5, '5.0 >= 5'\nassert 5 > 4.9, '5 > 4.9'\nassert 4.9 < 5, '4.9 < 5'\n\n# === Bool ordering (promotes to int) ===\nassert True > False, 'True > False'\nassert False < True, 'False < True'\nassert True >= 1, 'True >= 1'\nassert False <= 0, 'False <= 0'\nassert True > 0, 'True > 0'\nassert True < 2, 'True < 2'\nassert True > 0.5, 'True > 0.5'\nassert True < 1.5, 'True < 1.5'\nassert False < 0.5, 'False < 0.5'\nassert False >= -1, 'False >= -1'\n\n# === Cross-type non-equality ===\nassert 'hello' != 42, 'str != int'\nassert 42 != 'hello', 'int != str'\nassert b'hello' != 'hello', 'bytes != str'\nassert 'hello' != b'hello', 'str != bytes'\nassert None != 0, 'None != 0'\nassert 0 != None, '0 != None'\nassert [] != 'list', 'list != str'\nassert {} != 0, 'dict != int'\n\n# === LongInt cross-type comparisons ===\nbig = 2**100\nbig2 = 2**100\nassert big == big2, 'LongInt == LongInt'\nassert big != 5, 'LongInt != int'\nassert big > 5, 'LongInt > int'\nassert 5 < big, 'int < LongInt'\nassert big >= 5, 'LongInt >= int'\nassert 5 <= big, 'int <= LongInt'\nsmall_big = 2**100\nlarge_big = 2**101\nassert small_big < large_big, 'LongInt < LongInt'\nassert large_big > small_big, 'LongInt > LongInt'\nassert big != 'hello', 'LongInt != str'\n\n# === Bytes ordering ===\nassert b'abc' < b'abd', 'bytes lt'\nassert b'abc' <= b'abc', 'bytes le'\nassert b'abd' > b'abc', 'bytes gt'\nassert b'abc' >= b'abc', 'bytes ge'\nassert b'a' < b'b', 'single byte lt'\nassert b'' < b'a', 'empty bytes lt non-empty'\n\n# === String ordering ===\nassert 'abc' < 'abd', 'str lt'\nassert 'abc' <= 'abc', 'str le'\nassert 'abd' > 'abc', 'str gt'\nassert 'abc' >= 'abc', 'str ge'\nassert 'a' < 'b', 'single char lt'\n\n# === Heap-allocated string ordering (from split) ===\nparts = 'banana,apple'.split(',')\nassert parts[1] < parts[0], 'heap str lt'\nassert parts[0] > parts[1], 'heap str gt'\nassert parts[0] >= parts[0], 'heap str ge self'\nassert parts[0] <= parts[0], 'heap str le self'\n\n# === Cross-type string ordering (interned vs heap) ===\nheap_str = 'banana,apple'.split(',')[0]\nassert heap_str > 'apple', 'heap str gt interned'\nassert 'cherry' > heap_str, 'interned gt heap str'\nassert heap_str >= 'banana', 'heap str ge interned eq'\nassert 'banana' <= heap_str, 'interned le heap str eq'\n\n# === Containment: not in list ===\nassert 999 not in [1, 2, 3], 'not in list'\nassert 0 not in [1, 2, 3], 'zero not in list'\n\n# === Containment: not in tuple ===\nassert 'z' not in ('a', 'b', 'c'), 'not in tuple'\nassert 0 not in (1, 2, 3), 'zero not in tuple'\n\n# === Containment: in/not in set ===\nassert 2 in {1, 2, 3}, 'in set'\nassert 99 not in {1, 2, 3}, 'not in set'\n\n# === Containment: in/not in frozenset ===\nassert 2 in frozenset({1, 2, 3}), 'in frozenset'\nassert 99 not in frozenset({1, 2, 3}), 'not in frozenset'\n\n# === Containment: in/not in list (found) ===\nassert 2 in [1, 2, 3], 'in list'\nassert 'b' in ['a', 'b', 'c'], 'str in list'\n\n# === Containment: in/not in tuple (found) ===\nassert 'b' in ('a', 'b', 'c'), 'str in tuple'\nassert 2 in (1, 2, 3), 'int in tuple'\n"
  },
  {
    "path": "crates/monty/test_cases/comprehension__all.py",
    "content": "# === Basic list comprehension ===\nassert [x for x in [1, 2, 3]] == [1, 2, 3], 'identity'\nassert [x * 2 for x in [1, 2, 3]] == [2, 4, 6], 'transform'\nassert [x + 1 for x in range(5)] == [1, 2, 3, 4, 5], 'range'\n\n# === With filter ===\nassert [x for x in [1, 2, 3, 4] if x > 2] == [3, 4], 'filter'\nassert [x for x in [1, 2, 3, 4, 5] if x % 2 == 0] == [2, 4], 'even filter'\nassert [x for x in range(20) if x % 2 == 0 if x % 3 == 0] == [0, 6, 12, 18], 'multi-filter'\nassert [x * 2 for x in [1, 2, 3, 4] if x > 1 if x < 4] == [4, 6], 'transform with multi-filter'\n\n# === Nested for ===\nassert [x + y for x in [1, 2] for y in [10, 20]] == [11, 21, 12, 22], 'nested'\nassert [(x, y) for x in [1, 2] for y in ['a', 'b']] == [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')], 'nested tuple'\nassert [x * y for x in [1, 2, 3] for y in [10, 100]] == [10, 100, 20, 200, 30, 300], 'nested multiply'\n\n# === Nested with filter ===\nassert [x + y for x in [1, 2, 3] if x > 1 for y in [10, 20] if y > 10] == [22, 23], 'nested with filters'\n\n# === Set comprehension ===\nassert {x for x in [1, 2, 2, 3]} == {1, 2, 3}, 'set dedup'\nassert {x for x in [1, 2, 3] if x > 1} == {2, 3}, 'set filter'\nassert {x * 2 for x in [1, 2, 3]} == {2, 4, 6}, 'set transform'\nassert {x % 3 for x in range(10)} == {0, 1, 2}, 'set modulo'\n\n# === Dict comprehension ===\nassert {x: x * 2 for x in [1, 2, 3]} == {1: 2, 2: 4, 3: 6}, 'dict'\nassert {x: x for x in [1, 2, 3] if x > 1} == {2: 2, 3: 3}, 'dict filter'\nassert {str(x): x for x in [1, 2, 3]} == {'1': 1, '2': 2, '3': 3}, 'dict str keys'\nassert {x: y for x in [1, 2] for y in [10, 20]} == {1: 20, 2: 20}, 'dict nested overwrites'\n\n# === Scope isolation ===\nx = 'outer'\nresult = [x for x in [1, 2, 3]]\nassert x == 'outer', 'loop var does not leak'\n\ny = 'before'\nresult2 = [y * 2 for y in [1, 2]]\nassert y == 'before', 'loop var y does not leak'\n\n# === Access enclosing scope ===\nmultiplier = 10\nassert [x * multiplier for x in [1, 2]] == [10, 20], 'closure'\n\nprefix = 'item_'\nassert [prefix + str(x) for x in [1, 2, 3]] == ['item_1', 'item_2', 'item_3'], 'closure string'\n\nbase = [1, 2, 3]\nassert [x + 10 for x in base] == [11, 12, 13], 'closure list'\n\n\n# === Capture when iter uses same name as target ===\ndef outer_capture_same_name():\n    x = [1, 2, 3]\n\n    def inner():\n        return [x for x in x]\n\n    return inner()\n\n\nassert outer_capture_same_name() == [1, 2, 3], 'iter uses outer x'\n\n# === Empty iterables ===\nassert [x for x in []] == [], 'empty list'\nassert {x for x in []} == set(), 'empty set'\nassert {x: x for x in []} == {}, 'empty dict'\n\n# === Filter removes all ===\nassert [x for x in [1, 2, 3] if x > 10] == [], 'filter all'\nassert {x for x in [1, 2, 3] if x > 10} == set(), 'set filter all'\nassert {x: x for x in [1, 2, 3] if x > 10} == {}, 'dict filter all'\n\n# === Complex expressions ===\nassert [x**2 for x in [1, 2, 3, 4]] == [1, 4, 9, 16], 'square'\nassert [len(s) for s in ['a', 'bb', 'ccc']] == [1, 2, 3], 'len'\nassert [[y for y in range(x)] for x in [1, 2, 3]] == [[0], [0, 1], [0, 1, 2]], 'nested comprehension'\n\n# === Nested generator referencing prior loop var ===\n# Second generator's iter references first generator's loop variable\nassert [y for x in [[1, 2], [3, 4]] for y in x] == [1, 2, 3, 4], 'flatten nested lists'\nassert [(x, y) for x in [1, 2] for y in range(x)] == [(1, 0), (2, 0), (2, 1)], 'second iter uses first var'\n\n\ndef outer_nested_comp():\n    xs = [[1, 2], [3, 4]]\n\n    def inner():\n        return [y for x in xs for y in x]\n\n    return inner()\n\n\nassert outer_nested_comp() == [1, 2, 3, 4], 'nested comp in closure'\n\n# === Tuple unpacking in comprehensions ===\npairs = [(1, 'a'), (2, 'b'), (3, 'c')]\nassert [x for x, y in pairs] == [1, 2, 3], 'unpack first element'\nassert [y for x, y in pairs] == ['a', 'b', 'c'], 'unpack second element'\nassert [str(x) + str(y) for x, y in [(1, 2), (3, 4)]] == ['12', '34'], 'unpack and use both'\nassert [(y, x) for x, y in pairs] == [('a', 1), ('b', 2), ('c', 3)], 'swap unpacked elements'\n\n# Tuple unpacking with filter\nassert [x for x, y in pairs if x > 1] == [2, 3], 'unpack with filter'\nassert [y for x, y in pairs if y != 'b'] == ['a', 'c'], 'unpack filter on second'\n\n# Triple unpacking\ntriples = [(1, 2, 3), (4, 5, 6)]\nassert [a + b + c for a, b, c in triples] == [6, 15], 'triple unpack sum'\nassert [b for a, b, c in triples] == [2, 5], 'triple unpack middle'\n\n# Dict comprehension with unpacking\nd = {k: v for k, v in pairs}\nassert d == {1: 'a', 2: 'b', 3: 'c'}, 'dict comp with unpack'\nassert {v: k for k, v in pairs} == {'a': 1, 'b': 2, 'c': 3}, 'dict comp swap key/value'\n\n# Set comprehension with unpacking\nassert {x for x, y in pairs} == {1, 2, 3}, 'set comp unpack first'\nassert {y for x, y in pairs} == {'a', 'b', 'c'}, 'set comp unpack second'\n\n# Unpacking with dict.items()\nd2 = {'x': 10, 'y': 20, 'z': 30}\nassert [k for k, v in d2.items()] == ['x', 'y', 'z'], 'unpack dict items keys'\nassert [v for k, v in d2.items()] == [10, 20, 30], 'unpack dict items values'\nassert {v: k for k, v in d2.items()} == {10: 'x', 20: 'y', 30: 'z'}, 'dict comp invert dict'\n\n# Nested comprehension with unpacking\nmatrix = [[(1, 2), (3, 4)], [(5, 6), (7, 8)]]\nassert [[a + b for a, b in row] for row in matrix] == [[3, 7], [11, 15]], 'nested comp unpack'\n\n# Scope isolation with unpacking (vars don't leak)\nx = 'outer_x'\ny = 'outer_y'\nresult = [x + y for x, y in [(1, 2)]]\nassert x == 'outer_x', 'x does not leak from unpack'\nassert y == 'outer_y', 'y does not leak from unpack'\n\n\n# Unpacking in closure\ndef outer_unpack():\n    items = [(1, 2), (3, 4)]\n\n    def inner():\n        return [a * b for a, b in items]\n\n    return inner()\n\n\nassert outer_unpack() == [2, 12], 'unpack in closure'\n\n\n# Capture variable used in unpacking pattern\ndef outer_shadow_unpack():\n    x = 100\n\n    def inner():\n        # x in unpacking shadows the outer x, but we can still reference outer x in expression\n        # Actually, the x in the comprehension shadows outer x, so this tests scope isolation\n        pairs = [(1, 2), (3, 4)]\n        return [x + y for x, y in pairs]\n\n    return inner()\n\n\nassert outer_shadow_unpack() == [3, 7], 'shadow unpack in closure'\n\n# === Generator expressions (temporary: treated as list comprehensions) ===\n# TODO: When proper generators are implemented, these should return generator objects\n# instead of lists. For now, generator expressions are parsed as list comprehensions.\n# See iter__generator_expr.py for tests, and iter__generator_expr_type.py for\n# a type check test (xfail=cpython since CPython has real generators).\n\n# Generator in list() call - works identically in both Monty and CPython\nassert list(x for x in [1, 2, 3]) == [1, 2, 3], 'generator in list()'\nassert tuple(x for x in [1, 2, 3]) == (1, 2, 3), 'generator in tuple()'\n\n# Generator with condition\nassert list(x for x in range(10) if x % 2 == 0) == [0, 2, 4, 6, 8], 'generator with condition'\n\n# Nested generators\nassert list(x + y for x in range(3) for y in range(2)) == [0, 1, 1, 2, 2, 3], 'nested generator'\n\n# Generator in sum()\nassert sum(x for x in range(5)) == 10, 'generator in sum()'\n\n# Generator with unpacking\npairs = [(1, 2), (3, 4)]\nassert list(a + b for a, b in pairs) == [3, 7], 'generator with unpacking'\n\n# list of strings join\nassert ''.join(str(x) for x in range(5)) == '01234', 'list of strings join'\na = '1', '2', '3'\nassert ''.join(a) == '123', 'tuple of strings join'\n\n# === Regression: Iterator panic with try/except inside loop ===\n# Issue: https://github.com/pydantic/monty/issues/177\n# Verifies that exception handling in a comprehension inside a loop doesn't\n# corrupt the outer loop's iterator (causing \"expected Iterator on heap\" panic).\n# A prior loop is needed to potentially trigger incorrect stack depth tracking.\nfor _ in range(1):\n    pass\n\nfor s in ['hello']:\n    try:\n        # Inner comprehension raises exception\n        [int(c) for c in s]\n    except ValueError:\n        pass\n"
  },
  {
    "path": "crates/monty/test_cases/comprehension__scope.py",
    "content": "[x for x in range(10)]\n\ntry:\n    x\n    assert False, \"Expected NameError for 'x' after comprehension\"\nexcept NameError:\n    pass\n"
  },
  {
    "path": "crates/monty/test_cases/comprehension__unbound_local.py",
    "content": "# Test that comprehension scoping raises UnboundLocalError when a generator's iter\n# references a later generator's loop variable (which is local but not yet assigned)\n\nz = ['outer']\n\nresult = [x for x in [1] for y in z for z in [[2], [3]]]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"comprehension__unbound_local.py\", line 6, in <module>\n    result = [x for x in [1] for y in z for z in [[2], [3]]]\n                                      ~\nUnboundLocalError: cannot access local variable 'z' where it is not associated with a value\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/dataclass__basic.py",
    "content": "# call-external\n# === Basic dataclass tests ===\n\n# Get immutable dataclass from external function\npoint = make_point()\n\n# === repr and str ===\nassert repr(point) == 'Point(x=1, y=2)', f'point repr {point=!r}'\nassert str(point) == 'Point(x=1, y=2)', 'point str'\n\n# === Boolean truthiness ===\n# Dataclasses are always truthy (like Python class instances)\nassert bool(point), 'dataclass bool is True'\n\n# === Hash for immutable dataclass ===\n# Immutable (frozen) dataclasses are hashable\nh1 = hash(point)\nassert h1 != 0, 'hash is not zero'\n\n# Hash is consistent - same object hashes to same value\nh2 = hash(point)\nassert h1 == h2, 'hash is consistent'\n\n# Equal frozen dataclasses hash to same value\npoint2 = make_point()\nassert hash(point) == hash(point2), 'equal dataclasses have equal hash'\n\n# Frozen dataclass can be used as dict key\nd = {point: 'first'}\nassert d[point] == 'first', 'frozen dataclass as dict key'\nassert d[point2] == 'first', 'equal frozen dataclass looks up same value'\n\n# Frozen dataclass can be added to set\ns = {point, point2}\nassert len(s) == 1, 'equal frozen dataclasses deduplicated in set'\n\n# Different field values produce different hash\nalice = make_user('Alice')\nbob = make_user('Bob')\nassert hash(alice) != hash(bob), 'different field values have different hash'\n\n# === Mutable dataclass ===\nmut_point = make_mutable_point()\nassert repr(mut_point) == 'MutablePoint(x=1, y=2)', f'mutable point repr {mut_point=!r}'\n\n# === Dataclass with string argument ===\nalice = make_user('Alice')\nassert repr(alice) == \"User(name='Alice', active=True)\", f'user repr with string field {alice=!r}'\n\n# === Dataclass in list (using existing variables) ===\npoints = [point, mut_point, alice]\nassert len(points) == 3, 'dataclass list length'\n\n# === Attribute access (get) ===\n# Access fields on immutable dataclass\nassert point.x == 1, 'point.x is 1'\nassert point.y == 2, 'point.y is 2'\n\n# Access fields on mutable dataclass\nassert mut_point.x == 1, 'mut_point.x is 1'\nassert mut_point.y == 2, 'mut_point.y is 2'\n\n# Access fields on dataclass with string field\nassert alice.name == 'Alice', 'alice.name is Alice'\nassert alice.active == True, 'alice.active is True'\n\n# === Attribute assignment (set) ===\n# Modify mutable dataclass\nmut_point.x = 10\nassert mut_point.x == 10, 'mut_point.x updated to 10'\nmut_point.y = 20\nassert mut_point.y == 20, 'mut_point.y updated to 20'\nassert repr(mut_point) == 'MutablePoint(x=10, y=20)', f'repr after attribute update {mut_point=!r}'\n\n# === set other attributes\nmut_point.z = 30\nassert mut_point.z == 30, 'mut_point.z updated to 30'\nassert repr(mut_point) == 'MutablePoint(x=10, y=20)', 'repr after attribute update'\n\n# === Nested attribute access (chained get) ===\n# Create outer dataclass with inner dataclass as field\nouter = make_mutable_point()\ninner = make_mutable_point()\ninner.x = 100\ninner.y = 200\nouter.x = inner\n\n# Chained attribute get: outer.x.y\nassert outer.x.x == 100, 'outer.x.x is 100'\nassert outer.x.y == 200, 'outer.x.y is 200'\n\n# === Nested attribute assignment (chained set) ===\n# Modify nested field via chained access\nouter.x.x = 999\nassert outer.x.x == 999, 'outer.x.x updated to 999'\nouter.x.y = 888\nassert outer.x.y == 888, 'outer.x.y updated to 888'\n\n# Verify inner was modified (same object)\nassert inner.x == 999, 'inner.x also updated to 999'\nassert inner.y == 888, 'inner.y also updated to 888'\n\n# === Deeper nesting (3 levels) ===\nlevel1 = make_mutable_point()\nlevel2 = make_mutable_point()\nlevel3 = make_mutable_point()\nlevel3.x = 42\nlevel2.x = level3\nlevel1.x = level2\n\n# 3-level chained get\nassert level1.x.x.x == 42, 'level1.x.x.x is 42'\n\n# 3-level chained set\nlevel1.x.x.x = 7\nassert level1.x.x.x == 7, 'level1.x.x.x updated to 7'\nassert level3.x == 7, 'level3.x also updated to 7'\n\n# === Empty dataclass ===\nempty = make_empty()\nassert repr(empty) == 'Empty()', 'empty dataclass repr'\nassert str(empty) == 'Empty()', 'empty dataclass str'\n\n# === FrozenInstanceError is subclass of AttributeError ===\n# Catching AttributeError should also catch FrozenInstanceError\nfrozen_point = make_point()\ncaught = False\ntry:\n    frozen_point.x = 10\nexcept AttributeError:\n    caught = True\nassert caught, 'FrozenInstanceError caught by AttributeError'\n\n# === Error: accessing non-existent attribute ===\ntry:\n    point.nonexistent\n    assert False, 'should have raised AttributeError for missing attr'\nexcept AttributeError as e:\n    assert str(e) == \"'Point' object has no attribute 'nonexistent'\", f'wrong message: {e}'\n\n# === Error: accessing non-existent private attribute ===\ntry:\n    point._private\n    assert False, 'should have raised AttributeError for private attr'\nexcept AttributeError as e:\n    assert str(e) == \"'Point' object has no attribute '_private'\", f'wrong message: {e}'\n\n# === Error: calling a dunder that doesn't exist ===\ntry:\n    point.__nonexistent__()\n    assert False, 'should have raised AttributeError for dunder'\nexcept AttributeError as e:\n    assert str(e) == \"'Point' object has no attribute '__nonexistent__'\", f'wrong message: {e}'\n\n# === Error: calling a private method that doesn't exist ===\ntry:\n    point._private_method()\n    assert False, 'should have raised AttributeError for private method'\nexcept AttributeError as e:\n    assert str(e) == \"'Point' object has no attribute '_private_method'\", f'wrong message: {e}'\n\n# === Error: calling a field value (not callable) ===\ntry:\n    point.x()\n    assert False, 'should have raised TypeError for calling int field'\nexcept TypeError as e:\n    assert str(e) == \"'int' object is not callable\", f'wrong message: {e}'\n\n# === Error: calling a non-existent public method ===\ntry:\n    point.nonexistent_method()\n    assert False, 'should have raised AttributeError for missing method'\nexcept AttributeError as e:\n    assert str(e) == \"'Point' object has no attribute 'nonexistent_method'\", f'wrong message: {e}'\n\n# === Error: same errors on mutable dataclass ===\ntry:\n    mut_point.nonexistent\n    assert False, 'should have raised AttributeError on mutable dc'\nexcept AttributeError as e:\n    assert str(e) == \"'MutablePoint' object has no attribute 'nonexistent'\", f'wrong message: {e}'\n\ntry:\n    mut_point.x()\n    assert False, 'should have raised TypeError on mutable dc field call'\nexcept TypeError as e:\n    assert str(e) == \"'int' object is not callable\", f'wrong message: {e}'\n\n# === Method calls: no args (exercises ArgValues::prepend on Empty) ===\nresult = point.sum()\nassert result == 3, f'Point.sum() should be 3, got {result}'\n\n# === Method calls: two positional args (exercises ArgValues::prepend on Two) ===\nnew_point = point.add(10, 20)\nassert new_point.x == 11, f'Point.add x should be 11, got {new_point.x}'\nassert new_point.y == 22, f'Point.add y should be 22, got {new_point.y}'\n\n# === Method calls: one positional arg (exercises ArgValues::prepend on One) ===\nscaled = point.scale(3)\nassert scaled.x == 3, f'Point.scale x should be 3, got {scaled.x}'\nassert scaled.y == 6, f'Point.scale y should be 6, got {scaled.y}'\n\n# === Method calls: returning a string ===\ndesc = point.describe('pt')\nassert desc == 'pt(1, 2)', f'Point.describe should be pt(1, 2), got {desc}'\n\n# === Method calls on mutable dataclass ===\nmut_p2 = make_mutable_point()\nmut_sum = mut_p2.sum()\nassert mut_sum == 3, f'MutablePoint.sum() should be 3, got {mut_sum}'\n\n# === Method calls on User dataclass (string field) ===\nalice2 = make_user('Alice')\ngreeting = alice2.greeting()\nassert greeting == 'Hello, Alice!', f'User.greeting should be Hello, Alice!, got {greeting}'\n\n# === Method call returning dataclass - chained access ===\np3 = point.add(0, 0)\nassert p3.x == 1, f'chained method access: p3.x should be 1, got {p3.x}'\nassert p3.y == 2, f'chained method access: p3.y should be 2, got {p3.y}'\n\n# === Method calls with keyword-only args (exercises ArgValues::prepend on Kwargs) ===\ndesc_kw = point.describe(label='custom')\nassert desc_kw == 'custom(1, 2)', f'Point.describe(label=) should be custom(1, 2), got {desc_kw}'\n\n# === Error: calling non-existent method on mutable dataclass ===\ntry:\n    mut_p2.nonexistent_method()\n    assert False, 'should have raised AttributeError for missing method on mutable dc'\nexcept AttributeError as e:\n    assert str(e) == \"'MutablePoint' object has no attribute 'nonexistent_method'\", f'wrong message: {e}'\n\n# === Error: calling non-existent method on User ===\ntry:\n    alice2.missing()\n    assert False, 'should have raised AttributeError for missing method on User'\nexcept AttributeError as e:\n    assert str(e) == \"'User' object has no attribute 'missing'\", f'wrong message: {e}'\n"
  },
  {
    "path": "crates/monty/test_cases/dataclass__call_field_error.py",
    "content": "# call-external\n# Test that calling a field value (not a method) raises TypeError\npoint = make_point()\npoint.x()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"dataclass__call_field_error.py\", line 4, in <module>\n    point.x()\n    ~~~~~~~~~\nTypeError: 'int' object is not callable\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/dataclass__frozen_set_error.py",
    "content": "# call-external\n# Test that assigning to a frozen dataclass raises FrozenInstanceError\npoint = make_point()\npoint.x = 10\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"dataclass__frozen_set_error.py\", line 4, in <module>\n    point.x = 10\n    ~~~~~~~\nFrozenInstanceError: cannot assign to field 'x'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/dataclass__get_missing_attr_error.py",
    "content": "# call-external\n# Test that accessing a non-existent attribute on a dataclass raises AttributeError\npoint = make_point()\npoint.z\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"dataclass__get_missing_attr_error.py\", line 4, in <module>\n    point.z\nAttributeError: 'Point' object has no attribute 'z'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/dict__get_unhashable_key.py",
    "content": "d = {}\nd.get([], 'fallback')\n# Raise=TypeError(\"cannot use 'list' as a dict key (unhashable type: 'list')\")\n"
  },
  {
    "path": "crates/monty/test_cases/dict__literal_unhashable_key.py",
    "content": "{'a': 1, []: 2}\n# Raise=TypeError(\"cannot use 'list' as a dict key (unhashable type: 'list')\")\n"
  },
  {
    "path": "crates/monty/test_cases/dict__method_pop_missing_error.py",
    "content": "d = {'a': 1}\nd.pop('missing')\n# Raise=KeyError('missing')\n"
  },
  {
    "path": "crates/monty/test_cases/dict__methods.py",
    "content": "# === dict.clear() ===\nd = {'a': 1, 'b': 2}\nd.clear()\nassert d == {}, 'clear empties the dict'\n\nd = {}\nd.clear()\nassert d == {}, 'clear on empty dict is no-op'\n\n# === dict.copy() ===\nd = {'a': 1, 'b': 2}\ncopy = d.copy()\nassert copy == {'a': 1, 'b': 2}, 'copy creates equal dict'\nassert copy is not d, 'copy creates new dict object'\nd['c'] = 3\nassert 'c' not in copy, 'copy is independent'\n\nd = {}\ncopy = d.copy()\nassert copy == {}, 'copy empty dict'\n\n# === dict.update() ===\nd = {'a': 1}\nd.update({'b': 2})\nassert d == {'a': 1, 'b': 2}, 'update with dict'\n\nd = {'a': 1}\nd.update({'a': 10})\nassert d == {'a': 10}, 'update overwrites existing key'\n\nd = {'a': 1}\nd.update()\nassert d == {'a': 1}, 'update with no args is no-op'\n\nd = {}\nd.update([('a', 1), ('b', 2)])\nassert d == {'a': 1, 'b': 2}, 'update with list of tuples'\n\n# === dict.setdefault() ===\nd = {'a': 1}\nresult = d.setdefault('a', 10)\nassert result == 1, 'setdefault returns existing value'\nassert d == {'a': 1}, 'setdefault does not overwrite'\n\nd = {'a': 1}\nresult = d.setdefault('b', 2)\nassert result == 2, 'setdefault returns default for new key'\nassert d == {'a': 1, 'b': 2}, 'setdefault inserts new key'\n\nd = {'a': 1}\nresult = d.setdefault('b')\nassert result is None, 'setdefault default is None'\nassert d == {'a': 1, 'b': None}, 'setdefault inserts None'\n\n# === dict.popitem() ===\nd = {'a': 1, 'b': 2}\nitem = d.popitem()\nassert item == ('b', 2), 'popitem returns last inserted item'\nassert d == {'a': 1}, 'popitem removes item'\n\nd = {'x': 10}\nitem = d.popitem()\nassert item == ('x', 10), 'popitem on single-item dict'\nassert d == {}, 'dict is now empty'\n\n# === dict.fromkeys() ===\nd = dict.fromkeys(['a', 'b', 'c'])\nassert d == {'a': None, 'b': None, 'c': None}, 'fromkeys with list, default None'\n\nd = dict.fromkeys(['a', 'b'], 0)\nassert d == {'a': 0, 'b': 0}, 'fromkeys with default value'\n\nd = dict.fromkeys([])\nassert d == {}, 'fromkeys with empty iterable'\n\nd = dict.fromkeys('abc')\nassert d == {'a': None, 'b': None, 'c': None}, 'fromkeys with string iterable'\n\nd = dict.fromkeys(range(3), 'x')\nassert d == {0: 'x', 1: 'x', 2: 'x'}, 'fromkeys with range iterable'\n\nd = dict.fromkeys((1, 2, 3), [])\nassert d[1] is d[2] and d[2] is d[3], 'fromkeys shares same value object for all keys'\n\n# Duplicate keys - later occurrence wins\nd = dict.fromkeys(['a', 'b', 'a'], 1)\nassert d == {'a': 1, 'b': 1}, 'fromkeys with duplicate keys'\nassert list(d.keys()) == ['a', 'b'], 'fromkeys preserves first occurrence order'\n\n# === dict.fromkeys() instance access ===\n# fromkeys is a classmethod but should also work on instances\nd = {}.fromkeys(['a', 'b'])\nassert d == {'a': None, 'b': None}, 'fromkeys on empty dict instance'\n\nd = {'x': 1}.fromkeys(['a', 'b'], 0)\nassert d == {'a': 0, 'b': 0}, 'fromkeys on non-empty dict instance (ignores original)'\n\n# === dict.update() with keyword arguments ===\nd = {'a': 1}\nd.update(b=2)\nassert d == {'a': 1, 'b': 2}, 'update with single kwarg'\n\nd = {'a': 1}\nd.update(b=2, c=3)\nassert d == {'a': 1, 'b': 2, 'c': 3}, 'update with multiple kwargs'\n\nd = {'a': 1}\nd.update(a=10)\nassert d == {'a': 10}, 'update kwarg overwrites existing key'\n\nd = {}\nd.update(a=1, b=2)\nassert d == {'a': 1, 'b': 2}, 'update empty dict with kwargs'\n\n# update with both positional dict and kwargs\nd = {'a': 1}\nd.update({'b': 2}, c=3)\nassert d == {'a': 1, 'b': 2, 'c': 3}, 'update with dict and kwargs'\n\n# kwargs overwrite positional dict values\nd = {'a': 1}\nd.update({'b': 2}, b=20)\nassert d == {'a': 1, 'b': 20}, 'update kwargs overwrite positional dict'\n\n# update with iterable and kwargs\nd = {}\nd.update([('a', 1)], b=2)\nassert d == {'a': 1, 'b': 2}, 'update with iterable and kwargs'\n\n# === Error message for unknown classmethod ===\n# Error message should say 'dict' not 'type'\ntry:\n    dict.nonexistent()\n    assert False, 'should raise AttributeError'\nexcept AttributeError as e:\n    msg = str(e)\n    assert 'dict' in msg, f'error should mention dict, got: {e}'\n    assert 'nonexistent' in msg, f'error should mention method name, got: {e}'\n\n# === dict.update() sequence element error ===\n# Invalid sequence elements should raise ValueError\ntry:\n    d = {}\n    d.update([('a', 1), 'x', ('c', 3)])  # 'x' at index 1 is not a 2-tuple\n    assert False, 'should raise ValueError'\nexcept (ValueError, TypeError) as e:\n    msg = str(e)\n    # Error message should mention 'length' requirement\n    assert 'length' in msg.lower(), f'error should mention length, got: {e}'\n    # TODO: CPython includes element index (#N) in error message\n    # assert '#1' in msg, 'error should mention element index'\n"
  },
  {
    "path": "crates/monty/test_cases/dict__ops.py",
    "content": "# === Dict literals ===\nassert {} == {}, 'empty literal'\nassert {'a': 1} == {'a': 1}, 'single item literal'\nassert {'a': 1, 'b': 2} == {'a': 1, 'b': 2}, 'multiple items literal'\nassert {1: 'a', 2: 'b'} == {1: 'a', 2: 'b'}, 'int keys literal'\n\n# === Dict length ===\nassert len({}) == 0, 'len empty'\nassert len({'a': 1, 'b': 2, 'c': 3}) == 3, 'len multiple'\n\n# === Dict equality ===\nassert ({'a': 1, 'b': 2} == {'b': 2, 'a': 1}) == True, 'equality true (order independent)'\nassert ({'a': 1} == {'a': 2}) == False, 'equality false'\n\n# === Dict subscript get ===\nd = {'name': 'Alice', 'age': 30}\nassert d['name'] == 'Alice', 'subscript get str key'\nassert d['age'] == 30, 'subscript get value'\n\nd = {1: 'one', 2: 'two'}\nassert d[1] == 'one', 'subscript get int key'\n\n# === Dict subscript set ===\nd = {'a': 1}\nd['b'] = 2\nassert d == {'a': 1, 'b': 2}, 'subscript set new key'\n\nd = {'a': 1}\nd['a'] = 99\nassert d == {'a': 99}, 'subscript set existing key'\n\n# === Dict subscript augmented assignment ===\ntotals = {'photo': 1}\nrtype = 'photo'\nlikes = 2\ntotals[rtype] += likes\nassert totals == {'photo': 3}, 'subscript += updates existing dict item'\n\ncalls = 0\n\n\ndef key():\n    global calls\n    calls += 1\n    return 'photo'\n\n\ntotals = {'photo': 10}\ntotals[key()] += 5\nassert totals == {'photo': 15}, 'subscript += stores the computed result back'\nassert calls == 1, 'subscript += evaluates the index expression once'\n\ncaptured_total = {'photo': 1}\ncaptured_likes = 2\n\n\ndef apply_captured_increment():\n    captured_total['photo'] += captured_likes\n\n\napply_captured_increment()\nassert captured_total == {'photo': 3}, 'subscript += works with closure-captured names'\n\nwalrus_key = None\nwalrus_total = {'photo': 10}\nwalrus_total[(walrus_key := 'photo')] += 4\nassert walrus_key == 'photo', 'subscript += allows walrus in the index expression'\nassert walrus_total == {'photo': 14}, 'subscript += with walrus index updates the selected item'\n\ntry:\n    missing = {}\n    missing['photo'] += 1\n    assert False, 'subscript += on a missing dict key should raise KeyError'\nexcept KeyError as e:\n    assert e.args == ('photo',), 'subscript += missing key preserves the missing key in KeyError'\n\ntry:\n    existing = {'photo': 'a'}\n    existing['photo'] += 1\n    assert False, 'subscript += with incompatible operand types should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('can only concatenate str (not \"int\") to str',), 'subscript += type error matches CPython'\n    assert existing == {'photo': 'a'}, 'failed subscript += does not overwrite the original dict item'\n\n# === Dict.get() method ===\nd = {'a': 1, 'b': 2}\nassert d.get('a') == 1, 'get existing'\nassert d.get('missing') is None, 'get missing returns None'\nassert d.get('missing', 'default') == 'default', 'get missing with default'\n\n# === Dict.pop() method ===\nd = {'a': 1, 'b': 2}\nassert d.pop('a') == 1, 'pop existing'\nassert d == {'b': 2}, 'pop removes key'\n\nd = {'a': 1}\nassert d.pop('missing', 'default') == 'default', 'pop missing with default'\n\n# === Dict with tuple key ===\nd = {(1, 2): 'value'}\nassert d[(1, 2)] == 'value', 'tuple key'\n\n# === Dict repr ===\nassert repr({}) == '{}', 'empty repr'\nassert repr({'a': 1}) == \"{'a': 1}\", 'repr with items'\n\n# === Dict self-reference ===\nd = {}\nd['self'] = d\nassert d['self'] is d, 'getitem self'\n\nd = {}\nassert d.get('missing', d) is d, 'get default same dict'\n\n# === Dict unpacking (PEP 448) ===\na = {'x': 1, 'y': 2}\nb = {'y': 99, 'z': 3}\nassert {**a} == {'x': 1, 'y': 2}, 'single unpack'\nassert {**a, **b} == {'x': 1, 'y': 99, 'z': 3}, 'double unpack, later wins'\nassert {**a, 'y': 0} == {'x': 1, 'y': 0}, 'literal overrides unpacked key'\nassert {'y': 0, **a} == {'y': 2, 'x': 1}, 'unpack overrides earlier literal'\nassert {**a, 'z': 3} == {'x': 1, 'y': 2, 'z': 3}, 'unpack then new key'\nassert {**{}} == {}, 'unpack empty dict'\nassert {**a, **b, 'w': 4} == {'x': 1, 'y': 99, 'z': 3, 'w': 4}, 'complex mixed'\nassert list({**a, 'z': 3}.keys()) == ['x', 'y', 'z'], 'insertion order preserved'\n\n# === Dict unpack TypeError for non-mapping heap ref ===\n# Unpacking a Ref that is NOT a dict (e.g. a list) should raise TypeError\ntry:\n    x = {**[1, 2, 3]}\n    assert False, 'expected TypeError'\nexcept TypeError as e:\n    assert str(e) == \"'list' object is not a mapping\", f'wrong error: {e}'\n"
  },
  {
    "path": "crates/monty/test_cases/dict__pop_unhashable_key.py",
    "content": "# note cpython behaves weirdly if the dict is empty: https://github.com/python/cpython/issues/142396\nd = {1: 2}\nd.pop([], 'fallback')\n# Raise=TypeError(\"cannot use 'list' as a dict key (unhashable type: 'list')\")\n"
  },
  {
    "path": "crates/monty/test_cases/dict__popitem_empty.py",
    "content": "{}.popitem()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"dict__popitem_empty.py\", line 1, in <module>\n    {}.popitem()\n    ~~~~~~~~~~~~\nKeyError: 'popitem(): dictionary is empty'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/dict__subscript_missing_key.py",
    "content": "d = {'a': 1}\nd['missing']\n# Raise=KeyError('missing')\n"
  },
  {
    "path": "crates/monty/test_cases/dict__unhashable_dict_key.py",
    "content": "{{'a': 1}: 'value'}\n# Raise=TypeError(\"cannot use 'dict' as a dict key (unhashable type: 'dict')\")\n"
  },
  {
    "path": "crates/monty/test_cases/dict__unhashable_list_key.py",
    "content": "{[1, 2]: 'value'}\n# Raise=TypeError(\"cannot use 'list' as a dict key (unhashable type: 'list')\")\n"
  },
  {
    "path": "crates/monty/test_cases/dict__unpack_type_error.py",
    "content": "{**42}\n# Raise=TypeError(\"'int' object is not a mapping\")\n"
  },
  {
    "path": "crates/monty/test_cases/dict__views.py",
    "content": "# === Type identity and repr ===\nd = {'a': 1, 'b': 2}\n\nkeys = d.keys()\nitems = d.items()\nvalues = d.values()\n\nassert type(keys).__name__ == 'dict_keys', 'dict.keys() returns a dict_keys view'\nassert type(items).__name__ == 'dict_items', 'dict.items() returns a dict_items view'\nassert type(values).__name__ == 'dict_values', 'dict.values() returns a dict_values view'\n\nassert repr(keys) == \"dict_keys(['a', 'b'])\", 'keys repr matches CPython'\nassert repr(items) == \"dict_items([('a', 1), ('b', 2)])\", 'items repr matches CPython'\nassert repr(values) == 'dict_values([1, 2])', 'values repr matches CPython'\n\n# === len() and truthiness ===\nassert len(keys) == 2, 'keys view reports live dict length'\nassert len(items) == 2, 'items view reports live dict length'\nassert len(values) == 2, 'values view reports live dict length'\nassert bool(keys) is True, 'non-empty keys view is truthy'\nassert bool(items) is True, 'non-empty items view is truthy'\nassert bool(values) is True, 'non-empty values view is truthy'\nassert bool({}.keys()) is False, 'empty keys view is falsy'\nassert bool({}.items()) is False, 'empty items view is falsy'\nassert bool({}.values()) is False, 'empty values view is falsy'\n\n# === Iteration order ===\nassert list(keys) == ['a', 'b'], 'keys iterate in insertion order'\nassert list(items) == [('a', 1), ('b', 2)], 'items iterate in insertion order'\nassert list(values) == [1, 2], 'values iterate in insertion order'\n\n# === Membership ===\nassert ('a' in keys) is True, 'keys membership checks keys'\nassert ('missing' in keys) is False, 'keys membership is false for absent keys'\nassert (('a', 1) in items) is True, 'items membership matches existing pairs'\nassert (('a', 3) in items) is False, 'items membership checks values too'\nassert (('a',) in items) is False, 'items membership ignores non-2-tuples'\nassert (1 in values) is True, 'values membership checks stored values'\nassert (3 in values) is False, 'values membership is false for absent values'\n\ntry:\n    ([1], 'x') in {1: 'x'}.items()\n    assert False, 'items membership should reject unhashable keys'\nexcept TypeError as e:\n    assert str(e) == \"cannot use 'list' as a dict key (unhashable type: 'list')\", (\n        'items membership propagates key hash errors'\n    )\n\n# === Equality ===\nassert keys == keys, 'keys view equals itself'\nassert items == items, 'items view equals itself'\nassert values == values, 'values view equals itself by identity'\n\nassert keys == {'a', 'b'}, 'keys view compares equal to matching sets'\nassert {'b', 'a'} == keys, 'set equality works when dict_keys is on the right'\nassert keys == frozenset({'a', 'b'}), 'keys view compares equal to matching frozensets'\nassert frozenset({'a', 'b'}) == keys, 'frozenset equality works when dict_keys is on the right'\nassert keys == {'b': 0, 'a': 9}.keys(), 'keys view compares equal to another matching keys view'\nassert keys != {'a'}, 'keys view equality checks the full key set'\nassert keys != {'a', 'x'}, 'keys view inequality checks equal-length mismatches'\n\nassert items == {('a', 1), ('b', 2)}, 'items view compares equal to matching sets'\nassert {('b', 2), ('a', 1)} == items, 'set equality works when dict_items is on the right'\nassert items == frozenset({('a', 1), ('b', 2)}), 'items view compares equal to matching frozensets'\nassert frozenset({('a', 1), ('b', 2)}) == items, 'frozenset equality works when dict_items is on the right'\nassert items == {'b': 2, 'a': 1}.items(), 'items view compares equal to another matching items view'\nassert items != {('a', 1)}, 'items view equality checks the full item set'\nassert items != {('a', 2), ('b', 2)}, 'items view equality checks values as well as keys'\nassert items != {('a', 1), ('x', 9)}, 'items view inequality checks equal-length mismatches'\nassert ({'a': 1}.values() == {'a': 1}.values()) is False, 'distinct values views are never equal'\n\n# === Live behavior after mutation ===\nlive = {'x': 10}\nlive_keys = live.keys()\nlive_items = live.items()\nlive_values = live.values()\nlive['y'] = 20\n\nassert list(live_keys) == ['x', 'y'], 'keys view sees later insertions'\nassert list(live_items) == [('x', 10), ('y', 20)], 'items view sees later insertions'\nassert list(live_values) == [10, 20], 'values view sees later insertions'\nassert repr(live_keys) == \"dict_keys(['x', 'y'])\", 'keys repr stays live after mutation'\nassert len(live_values) == 2, 'values len updates after mutation'\n\n# === Dict mutation during iteration ===\nchanging = {'a': 1, 'b': 2}\nchanging_iter = iter(changing.keys())\nassert next(changing_iter) == 'a', 'iterator yields the first key before mutation'\nchanging['c'] = 3\ntry:\n    next(changing_iter)\n    assert False, 'changing dict size during keys iteration should raise'\nexcept RuntimeError as e:\n    assert str(e) == 'dictionary changed size during iteration', 'keys iteration error matches CPython'\n\nchanging = {'a': 1, 'b': 2}\nchanging_iter = iter(changing.items())\nassert next(changing_iter) == ('a', 1), 'iterator yields the first item before mutation'\nchanging['c'] = 3\ntry:\n    next(changing_iter)\n    assert False, 'changing dict size during items iteration should raise'\nexcept RuntimeError as e:\n    assert str(e) == 'dictionary changed size during iteration', 'items iteration error matches CPython'\n\nchanging = {'a': 1, 'b': 2}\nchanging_iter = iter(changing.values())\nassert next(changing_iter) == 1, 'iterator yields the first value before mutation'\nchanging['c'] = 3\ntry:\n    next(changing_iter)\n    assert False, 'changing dict size during values iteration should raise'\nexcept RuntimeError as e:\n    assert str(e) == 'dictionary changed size during iteration', 'values iteration error matches CPython'\n\n# === dict_keys & iterable ===\nd = {'a': 1, 'b': 2, 'c': 3}\nassert d.keys() & {'b', 'c', 'x'} == {'b', 'c'}, 'keys view intersects sets'\nassert d.keys() & ('b', 'x', 'a') == {'a', 'b'}, 'keys view intersects tuples'\nassert d.keys() & iter(['c', 'c', 'a']) == {'a', 'c'}, 'keys view intersects iterators'\nassert type(d.keys() & {'a'}).__name__ == 'set', 'keys intersection returns a plain set'\n\ntry:\n    d.keys() & 1\n    assert False, 'keys intersection should reject non-iterables'\nexcept TypeError as e:\n    assert str(e) == \"'int' object is not iterable\", 'non-iterable rhs error matches CPython'\n\n# === dict_keys set-like operators ===\nassert d.keys() | ('c', 'd') == {'a', 'b', 'c', 'd'}, 'keys view unions arbitrary iterables'\nassert d.keys() ^ ('b', 'd', 'e') == {'a', 'c', 'd', 'e'}, 'keys view symmetric difference works'\nassert d.keys() - ('b', 'd') == {'a', 'c'}, 'keys view difference works'\nassert d.keys() & {'b': 0, 'z': 9}.keys() == {'b'}, 'keys view intersects other keys views'\nassert d.keys() | {'c': 0, 'd': 1}.keys() == {'a', 'b', 'c', 'd'}, 'keys view unions other keys views'\nassert d.keys().isdisjoint(['x', 'y']) is True, 'keys isdisjoint accepts arbitrary iterables'\nassert d.keys().isdisjoint(iter(['x', 'a'])) is False, 'keys isdisjoint consumes iterators'\n\n# === dict_items set-like operators ===\nitems_dict = {'a': 1, 'b': 2}\nassert items_dict.items() & [('a', 1), ('x', 9)] == {('a', 1)}, 'items view intersects iterables of pairs'\nassert items_dict.items() | [('c', 3)] == {('a', 1), ('b', 2), ('c', 3)}, 'items view unions iterables of pairs'\nassert items_dict.items() ^ [('a', 1), ('c', 3)] == {('b', 2), ('c', 3)}, 'items view symmetric difference works'\nassert items_dict.items() - [('a', 1)] == {('b', 2)}, 'items view difference works'\nassert items_dict.items() & {'b': 2, 'x': 9}.items() == {('b', 2)}, 'items view intersects other items views'\nassert items_dict.items().isdisjoint([('x', 1)]) is True, 'items isdisjoint accepts arbitrary iterables'\nassert items_dict.items().isdisjoint(iter([('a', 1)])) is False, 'items isdisjoint consumes iterators'\n\n# === dict_values remains non-set-like ===\ntry:\n    {'a': 1}.values() & [1]\n    assert False, 'dict_values should not support set-like operators'\nexcept TypeError:\n    pass\n\ntry:\n    {'a': 1}.values().isdisjoint([1])\n    assert False, 'dict_values should not gain isdisjoint'\nexcept AttributeError:\n    pass\n\n# === Motivating milestone example ===\nme_map = {'me': 1, 'you': 2, 'merve': 3}\nmerve_set = {'merve', 'unknown'}\ncommon_ids = me_map.keys() & merve_set\nassert common_ids == {'merve'}, 'dict_keys & set supports the motivating use case'\n"
  },
  {
    "path": "crates/monty/test_cases/edge__all.py",
    "content": "# === Empty container lengths ===\nassert (len([]), len(()), len('')) == (0, 0, 0), 'all empty lengths'\n\n# === Large concatenations ===\nlst = []\nfor i in range(100):\n    lst += [i]\nassert len(lst) == 100, 'large list concat'\n\ns = ''\nfor i in range(100):\n    s += 'x'\nassert len(s) == 100, 'large string concat'\n\n# === Self-concatenation ===\nlst = [1]\nlst += lst\nlst += lst\nassert lst == [1, 1, 1, 1], 'list self concat twice'\n\n# === Mod comparison in loop ===\ncount = 0\nfor i in range(100):\n    if i % 7 == 0:\n        count += 1\nassert count == 15, 'mod comparison chain'\n"
  },
  {
    "path": "crates/monty/test_cases/edge__float_int_mod.py",
    "content": "7.5 % 2\n# Return=1.5\n"
  },
  {
    "path": "crates/monty/test_cases/edge__int_float_mod.py",
    "content": "7 % 2.5\n# Return=2.0\n"
  },
  {
    "path": "crates/monty/test_cases/exc__args.py",
    "content": "# === Exception .args attribute ===\ntry:\n    raise ValueError('test message')\nexcept ValueError as e:\n    assert e.args == ('test message',), 'args is tuple with message'\n    assert e.args[0] == 'test message', 'args[0] is the message'\n\ntry:\n    raise ValueError()\nexcept ValueError as e:\n    assert e.args == (), 'no-arg exception has empty args'\n\ntry:\n    raise TypeError('type error')\nexcept TypeError as e:\n    assert e.args[0] == 'type error', 'works for other exception types'\n"
  },
  {
    "path": "crates/monty/test_cases/exc__str.py",
    "content": "try:\n    raise ValueError\nexcept ValueError as e:\n    assert str(e) == ''\n    assert repr(e) == 'ValueError()'\nelse:\n    raise AssertionError('should raise an error')\n\ntry:\n    raise ValueError()\nexcept ValueError as e:\n    assert str(e) == ''\n    assert repr(e) == 'ValueError()'\nelse:\n    raise AssertionError('should raise an error')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_ok__all.py",
    "content": "# === Basic arithmetic ===\nassert 1 + 1 == 2, 'add ints'\nassert 'a' + 'b' == 'ab', 'add strs'\n\n# === Equality tests ===\nassert (1 == 1) == True, 'ints equal true'\nassert (1 == 2) == False, 'ints equal false'\nassert ('a' == 'a') == True, 'str equal'\nassert ('a' == 'b') == False, 'str not equal'\nassert ([1, 2] == [1, 2]) == True, 'list equal'\nassert ((1, 2) == (1, 2)) == True, 'tuple equal'\nassert (b'hello' == b'hello') == True, 'bytes equal'\n\n# === Boolean repr/str ===\nassert repr(True) == 'True', 'bool true repr'\nassert str(True) == 'True', 'bool true str'\nassert repr(False) == 'False', 'bool false repr'\nassert str(False) == 'False', 'bool false str'\n\n# === None repr/str ===\nassert repr(None) == 'None', 'none repr'\nassert str(None) == 'None', 'none str'\n\n# === Ellipsis repr/str ===\nassert repr(...) == 'Ellipsis', 'ellipsis repr'\nassert str(...) == 'Ellipsis', 'ellipsis str'\n\n# === List repr/str ===\nassert repr([1, 2]) == '[1, 2]', 'list repr'\nassert str([1, 2]) == '[1, 2]', 'list str'\n\n# === Discard expression result ===\na = 1\n[1, 2, 3]  # this list is created and discarded\nassert a == 1, 'discard list'\n\n# === Shared list append ===\na = [1]\nb = a\nb.append(2)\nassert len(a) == 2, 'shared list append'\n\n# === For loop string append ===\nv = ''\nfor i in range(1000):\n    if i % 13 == 0:\n        v = v + 'x'\nassert len(v) == 77, 'for loop str append assign'\n\nv = ''\nfor i in range(1000):\n    if i % 13 == 0:\n        v += 'x'\nassert len(v) == 77, 'for loop str append assign op'\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__error_instance_str.py",
    "content": "raise ValueError('testing')\n# Raise=ValueError('testing')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__error_no_args.py",
    "content": "raise TypeError()\n# Raise=TypeError()\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__error_string_arg.py",
    "content": "raise TypeError('hello')\n# Raise=TypeError('hello')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__error_string_arg_quotes.py",
    "content": "raise TypeError(\"hello 'there'\")\n# Raise=TypeError(\"hello 'there'\")\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__error_type.py",
    "content": "raise TypeError\n# Raise=TypeError()\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__raise_instance_via_var.py",
    "content": "# raise exception instance stored in a variable\na = ValueError('instance error')\nraise a\n# Raise=ValueError('instance error')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__raise_list.py",
    "content": "raise []\n# Raise=TypeError('exceptions must derive from BaseException')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__raise_number.py",
    "content": "raise 1 + 2\n# Raise=TypeError('exceptions must derive from BaseException')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__raise_type_call_via_var.py",
    "content": "# raise exception type called via variable\na = ValueError\nraise a('error message')\n# Raise=ValueError('error message')\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__raise_type_direct.py",
    "content": "# raise exception type directly\nraise ValueError\n# Raise=ValueError()\n"
  },
  {
    "path": "crates/monty/test_cases/execute_raise__raise_type_via_var.py",
    "content": "# raise exception type stored in a variable\na = ValueError\nraise a\n# Raise=ValueError()\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__arg_side_effect_bug.py",
    "content": "# call-external\n# BUG: Side effects in arguments are duplicated when external call suspends\n#\n# When an external call is in one argument position and there's a side effect\n# in another argument position, the side effect may be executed multiple times\n# because argument evaluation is repeated during resumption.\n\ncall_count = 0\n\n\ndef side_effect(val):\n    global call_count\n    call_count += 1\n    return val\n\n\n# Side effect before external call in args\n# Expected: side_effect runs once, result is 10 + 3 = 13\ncall_count = 0\nresult = add_ints(side_effect(10), add_ints(1, 2))\nassert result == 13, 'ext call after side effect'\nassert call_count == 1, 'side effect should happen only once (before ext)'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__augmented.py",
    "content": "# call-external\n# External calls in augmented assignment expressions\n\n# += with external call\nx = 10\nx += add_ints(5, 5)\nassert x == 20, 'ext call in augmented add'\n\n# -= with external call\nx = 100\nx -= add_ints(20, 30)\nassert x == 50, 'ext call in augmented sub'\n\n# *= with external call\nx = 5\nx *= add_ints(2, 1)\nassert x == 15, 'ext call in augmented mul'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__augmented_refcount_bug.py",
    "content": "# call-external\n# BUG: Reference counting bug with string augmented assignment and external calls\n\n# String += with external call causes reference counting error\ns = 'hello'\ns += concat_strings(' ', 'world')\nassert s == 'hello world', 'ext call in augmented string concat'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__bare_raise_after_resume.py",
    "content": "# call-external\n# Test bare raise after external call resumption in except handler\n# This tests that current_exception is properly restored after resuming\n# Note: bare raise after resumption only works when exception is bound (as e)\n\ncaught_reraised = False\ntry:\n    try:\n        raise ValueError('original error')\n    except ValueError as e:\n        # Make an external call, which will cause a suspend/resume\n        x = add_ints(1, 2)\n        # After resuming, bare raise should still work (exception restored from binding)\n        raise\nexcept ValueError as outer_e:\n    caught_reraised = repr(outer_e) == \"ValueError('original error')\"\n\nassert caught_reraised, 'bare raise after external call should re-raise original exception'\n\n# === Nested handler bare raise after resumption ===\nouter_nested_reraise = False\ntry:\n    try:\n        raise ValueError('outer error')\n    except ValueError:\n        try:\n            raise KeyError('inner error')\n        except KeyError:\n            _ = add_ints(1, 2)\n        raise\nexcept ValueError as reraised:\n    outer_nested_reraise = repr(reraised) == \"ValueError('outer error')\"\n\nassert outer_nested_reraise, 'bare raise in outer handler should re-raise original exception after nested resumption'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__basic.py",
    "content": "# call-external\n# === Basic external function tests ===\n\n# Simple calls\na = add_ints(10, 20)\nassert a == 30, 'add_ints basic'\n\nb = add_ints(-5, 15)\nassert b == 10, 'add_ints with negative'\n\ns = concat_strings('hello', ' world')\nassert s == 'hello world', 'concat_strings basic'\n\nx = return_value(42)\nassert x == 42, 'return_value with int'\n\ny = return_value('test')\nassert y == 'test', 'return_value with str'\n\n# === Assignment with external calls ===\nresult = add_ints(100, 200)\nassert result == 300, 'assignment from add_ints'\n\nname = concat_strings('foo', 'bar')\nassert name == 'foobar', 'assignment from concat_strings'\n\n# === Nested calls ===\nnested = add_ints(1, add_ints(2, 3))\nassert nested == 6, 'nested add_ints right'\n\nnested2 = add_ints(add_ints(1, 2), 3)\nassert nested2 == 6, 'nested add_ints left'\n\nnested3 = add_ints(add_ints(1, 2), add_ints(3, 4))\nassert nested3 == 10, 'nested add_ints both'\n\ndeep = add_ints(add_ints(add_ints(1, 2), 3), 4)\nassert deep == 10, 'deeply nested add_ints'\n\n# === Chained operations ===\nchained = add_ints(1, 2) + add_ints(3, 4)\nassert chained == 10, 'chained add_ints with +'\n\nchained2 = add_ints(10, 20) - add_ints(5, 10)\nassert chained2 == 15, 'chained add_ints with -'\n\nchained3 = add_ints(2, 3) * add_ints(4, 5)\nassert chained3 == 45, 'chained add_ints with *'\n\nstr_chain = concat_strings('a', 'b') + concat_strings('c', 'd')\nassert str_chain == 'abcd', 'chained concat_strings'\n\n# === External calls in assert statements ===\nassert add_ints(5, 5) == 10, 'ext call in assert condition'\nassert return_value(True), 'ext call returning truthy in assert'\nassert concat_strings('x', 'y') == 'xy', 'concat in assert'\nassert add_ints(1, add_ints(2, 3)) == 6, 'nested ext call in assert'\n\n# === Mixed with builtins ===\nlength = len(concat_strings('hello', 'world'))\nassert length == 10, 'len of concat result'\n\nitems = [add_ints(1, 2), add_ints(3, 4)]\nassert items[0] == 3, 'ext call in list literal first'\nassert items[1] == 7, 'ext call in list literal second'\n\n# === Multiple external calls in single expression ===\n\n# Two ext calls added together\nsum_result = add_ints(1, 2) + add_ints(3, 4)\nassert sum_result == 10, 'two ext calls in addition'\n\n# Three ext calls in one expression\ntriple = add_ints(1, 1) + add_ints(2, 2) + add_ints(3, 3)\nassert triple == 12, 'three ext calls in expression'\n\n# Ext calls in multiplication\nmul_result = add_ints(2, 3) * add_ints(1, 1)\nassert mul_result == 10, 'ext calls in multiplication'\n\n# Ext calls in subtraction\nsub_result = add_ints(10, 5) - add_ints(3, 2)\nassert sub_result == 10, 'ext calls in subtraction'\n\n# Complex expression with multiple ext calls\ncomplex_expr = (add_ints(1, 2) + add_ints(3, 4)) * add_ints(0, 2)\nassert complex_expr == 20, 'complex expr with ext calls'\n\n# String concatenation with multiple ext calls\nstr_result = concat_strings(return_value('a'), return_value('b')) + concat_strings('c', 'd')\nassert str_result == 'abcd', 'multiple ext calls for string concat'\n\n# Comparison with multiple ext calls\ncmp_result = add_ints(5, 5) == add_ints(3, 7)\nassert cmp_result == True, 'comparison of two ext call results'\n\n# Nested ext calls in expression\nnested_expr = add_ints(add_ints(1, 2), add_ints(3, 4))\nassert nested_expr == 10, 'nested ext calls in expression'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__boolean.py",
    "content": "# call-external\n# External calls in boolean short-circuit expressions\n\n# === Basic boolean operations ===\nresult = return_value(True) and return_value(42)\nassert result == 42, 'ext call in and (both run)'\n\nresult = return_value(False) and return_value(42)\nassert result == False, 'ext call in and (short circuit)'\n\nresult = return_value(0) or return_value(42)\nassert result == 42, 'ext call in or (both run)'\n\nresult = return_value(99) or return_value(42)\nassert result == 99, 'ext call in or (short circuit)'\n\n\n# === Chained boolean with external calls ===\nresult = return_value(True) and return_value(True) and return_value(42)\nassert result == 42, 'chained and all truthy'\n\nresult = return_value(True) and return_value(False) and return_value(42)\nassert result == False, 'chained and with false in middle'\n\nresult = return_value(0) or return_value(0) or return_value(42)\nassert result == 42, 'chained or all falsy except last'\n\nresult = return_value(0) or return_value(99) or return_value(42)\nassert result == 99, 'chained or with truthy in middle'\n\n\n# === Mixed and/or ===\nresult = return_value(True) and return_value(0) or return_value(42)\nassert result == 42, 'and then or'\n\nresult = return_value(0) or return_value(True) and return_value(42)\nassert result == 42, 'or then and'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__boolean_side_effect_hang.py",
    "content": "# call-external\n# BUG: This test hangs (infinite loop) - external calls in boolean expressions\n# with side effects cause incorrect behavior.\n\ncall_count = 0\n\n\ndef side_effect(val):\n    global call_count\n    call_count += 1\n    return val\n\n\n# This specific pattern causes a hang in Monty\nresult = return_value(True) and return_value(side_effect(42))\nassert result == 42, 'and: second runs when first truthy'\nassert call_count == 1, 'and: side effect runs when first is truthy'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__closure_bug.py",
    "content": "# call-external\n# BUG: External calls in closures cannot access captured variables\n# When an external call is inside a closure, it fails to access free variables\n# Error: \"cannot access free variable 'x' where it is not associated with a value\"\n\n\ndef make_adder(x):\n    def adder(y):\n        return add_ints(x, y)\n\n    return adder\n\n\nadd5 = make_adder(5)\nresult = add5(10)\nassert result == 15, 'ext call in closure accessing captured var'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__comparison.py",
    "content": "# call-external\n# External calls in comparison expressions\n\n# External call on left side of comparison\nresult = add_ints(1, 2) == 3\nassert result == True, 'ext call == literal'\n\n# External call on right side\nresult = 3 == add_ints(1, 2)\nassert result == True, 'literal == ext call'\n\n# Both sides external calls\nresult = add_ints(1, 2) == add_ints(2, 1)\nassert result == True, 'ext call == ext call'\n\n# Less than\nresult = add_ints(1, 1) < add_ints(2, 2)\nassert result == True, 'ext call < ext call'\n\n# Greater than\nresult = add_ints(5, 5) > add_ints(2, 2)\nassert result == True, 'ext call > ext call'\n\n# Not equal\nresult = add_ints(1, 2) != add_ints(3, 4)\nassert result == True, 'ext call != ext call'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__deep_call_stack.py",
    "content": "# call-external\n# External function calls in deep call stacks (function calling function).\n# Tests that the outer function receives the return value correctly when\n# the inner function makes an external call.\n\n\ndef depth1(n):\n    return add_ints(n, 1)\n\n\ndef depth2(n):\n    return depth1(n) + 10\n\n\nresult = depth2(5)\n# depth2(5) should be: depth1(5) + 10 = 6 + 10 = 16\n\nassert result == 16, f'ext call through 2 levels of functions {result=}'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__elif.py",
    "content": "# call-external\n# === External calls in elif conditions ===\n\n# External call in elif condition - true\nresult1 = 0\nif add_ints(1, 1) == 3:\n    result1 = 1\nelif add_ints(2, 2) == 4:\n    result1 = 2\nelse:\n    result1 = 3\nassert result1 == 2, f'elif condition with ext call should be evaluated, {result1=}'\n\n# External call in elif condition - falls through to else\nresult2 = 0\nif add_ints(1, 1) == 3:\n    result2 = 1\nelif add_ints(2, 2) == 5:\n    result2 = 2\nelse:\n    result2 = 3\nassert result2 == 3, 'else taken when all ext call conditions are false'\n\n# Multiple elif with external calls\nresult3 = 0\nif add_ints(1, 1) == 10:\n    result3 = 1\nelif add_ints(2, 2) == 10:\n    result3 = 2\nelif add_ints(3, 3) == 6:\n    result3 = 3\nelif add_ints(4, 4) == 10:\n    result3 = 4\nelse:\n    result3 = 5\nassert result3 == 3, 'third elif with ext call should match'\n\n# === External calls in elif bodies ===\n\n# Ext call in elif body\nval1 = 0\nif False:\n    val1 = 1\nelif True:\n    val1 = add_ints(10, 20)\nelse:\n    val1 = 3\nassert val1 == 30, 'ext call in elif body'\n\n# Ext call in else body after elif chain\nval2 = 0\nif False:\n    val2 = 1\nelif False:\n    val2 = 2\nelse:\n    val2 = add_ints(15, 25)\nassert val2 == 40, 'ext call in else body after elif'\n\n# Multiple ext calls in elif body\nval3 = 0\nif False:\n    val3 = 1\nelif True:\n    a = add_ints(5, 5)\n    b = add_ints(10, 10)\n    val3 = add_ints(a, b)\nelse:\n    val3 = 3\nassert val3 == 30, 'multiple ext calls in elif body'\n\n# === Nested ext calls ===\n\n# Nested ext calls in elif condition\nresult4 = 0\nif False:\n    result4 = 1\nelif add_ints(add_ints(1, 2), add_ints(3, 4)) == 10:\n    result4 = 2\nelse:\n    result4 = 3\nassert result4 == 2, 'nested ext calls in elif condition'\n\n# === Short-circuit with ext calls ===\n\n# Ext call should not be evaluated if earlier condition is true\ncall_count = 0\n\n\ndef counting_add(a, b):\n    global call_count\n    call_count = call_count + 1\n    return a + b\n\n\n# This uses a regular function, not ext call, to verify short-circuit\n# but the ext calls in bodies still test suspension\nx = 0\nif True:\n    x = add_ints(1, 1)\nelif False:\n    x = add_ints(2, 2)\nassert x == 2, 'if body ext call executed, elif skipped'\n\n# === Ext call in both condition and body ===\n\nresult5 = 0\nif add_ints(1, 1) == 3:\n    result5 = add_ints(100, 100)\nelif add_ints(2, 2) == 4:\n    result5 = add_ints(50, 50)\nelse:\n    result5 = add_ints(25, 25)\nassert result5 == 100, 'ext call in both elif condition and body'\n\n# === Ext call in if body when condition is true ===\n\nif_body_result = 0\nif add_ints(5, 5) == 10:\n    if_body_result = add_ints(100, 200)\nelif add_ints(1, 1) == 2:\n    if_body_result = add_ints(10, 20)\nelse:\n    if_body_result = add_ints(1, 2)\nassert if_body_result == 300, 'ext call in if body when if condition is true'\n\n# === Ext calls returning values used as conditions ===\n\n# return_value returns its argument, so we can use it to test boolean coercion\ncond_result = 0\nif return_value(0):\n    cond_result = 1\nelif return_value(1):\n    cond_result = 2\nelse:\n    cond_result = 3\nassert cond_result == 2, 'ext call return value used as boolean condition'\n\n# === Ext calls with string concatenation ===\n\nstr_result = ''\nif add_ints(1, 1) == 3:\n    str_result = concat_strings('a', 'b')\nelif add_ints(2, 2) == 4:\n    str_result = concat_strings('hello', ' world')\nelse:\n    str_result = concat_strings('x', 'y')\nassert str_result == 'hello world', 'ext call with string result in elif body'\n\n# === Multiple conditions with ext calls in same expression ===\n\nmulti_cond = 0\nif add_ints(1, 1) > 5:\n    multi_cond = 1\nelif add_ints(2, 2) < add_ints(3, 3):\n    multi_cond = 2\nelse:\n    multi_cond = 3\nassert multi_cond == 2, 'comparison between two ext call results in elif condition'\n\n# === Ext call in all three branches ===\n\nall_branches = 0\nval = add_ints(5, 5)\nif val < 5:\n    all_branches = add_ints(1, 0)\nelif val < 15:\n    all_branches = add_ints(2, 0)\nelse:\n    all_branches = add_ints(3, 0)\nassert all_branches == 2, 'ext call in elif body based on earlier ext call result'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__exc.py",
    "content": "# call-external\n# External call in raise statement\nraise ValueError(return_value('foobar'))\n# Raise=ValueError('foobar')\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__exc_deep_stack.py",
    "content": "# call-external\ndef level4():\n    x = 1\n    raise_error('RuntimeError', 'deep error')\n\n\ndef level3():\n    level4()\n\n\ndef level2():\n    level3()\n\n\ndef level1():\n    level2()\n\n\nlevel1()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"ext_call__exc_deep_stack.py\", line 19, in <module>\n    level1()\n    ~~~~~~~~\n  File \"ext_call__exc_deep_stack.py\", line 16, in level1\n    level2()\n    ~~~~~~~~\n  File \"ext_call__exc_deep_stack.py\", line 12, in level2\n    level3()\n    ~~~~~~~~\n  File \"ext_call__exc_deep_stack.py\", line 8, in level3\n    level4()\n    ~~~~~~~~\n  File \"ext_call__exc_deep_stack.py\", line 4, in level4\n    raise_error('RuntimeError', 'deep error')\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nRuntimeError: deep error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__exc_in_function.py",
    "content": "# call-external\ndef wrapper():\n    raise_error('ValueError', 'from external')\n\n\nwrapper()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"ext_call__exc_in_function.py\", line 6, in <module>\n    wrapper()\n    ~~~~~~~~~\n  File \"ext_call__exc_in_function.py\", line 3, in wrapper\n    raise_error('ValueError', 'from external')\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nValueError: from external\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__exc_nested_functions.py",
    "content": "# call-external\ndef inner():\n    raise_error('TypeError', 'nested error')\n\n\ndef middle():\n    inner()\n\n\ndef outer():\n    middle()\n\n\nouter()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"ext_call__exc_nested_functions.py\", line 14, in <module>\n    outer()\n    ~~~~~~~\n  File \"ext_call__exc_nested_functions.py\", line 11, in outer\n    middle()\n    ~~~~~~~~\n  File \"ext_call__exc_nested_functions.py\", line 7, in middle\n    inner()\n    ~~~~~~~\n  File \"ext_call__exc_nested_functions.py\", line 3, in inner\n    raise_error('TypeError', 'nested error')\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nTypeError: nested error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__ext_exc.py",
    "content": "# call-external\n# === External function exceptions ===\n# Tests for exceptions raised by external functions\n\n# === Basic exception propagation ===\n\n# External function raising ValueError\ncaught_value_error = False\ntry:\n    result = raise_error('ValueError', 'test error')\n    assert False, 'should not reach here'\nexcept ValueError:\n    caught_value_error = True\nassert caught_value_error, 'ValueError was caught'\n\n# External function raising TypeError\ncaught_type_error = False\ntry:\n    result = raise_error('TypeError', 'type error message')\n    assert False, 'should not reach here'\nexcept TypeError:\n    caught_type_error = True\nassert caught_type_error, 'TypeError was caught'\n\n# External function raising KeyError\ncaught_key_error = False\ntry:\n    result = raise_error('KeyError', 'missing key')\n    assert False, 'should not reach here'\nexcept KeyError:\n    caught_key_error = True\nassert caught_key_error, 'KeyError was caught'\n\n# External function raising RuntimeError\ncaught_runtime_error = False\ntry:\n    result = raise_error('RuntimeError', 'runtime error')\n    assert False, 'should not reach here'\nexcept RuntimeError:\n    caught_runtime_error = True\nassert caught_runtime_error, 'RuntimeError was caught'\n\n# === Exception not caught by wrong handler ===\n\n# ValueError not caught by TypeError handler\ncaught_outer = False\ntry:\n    try:\n        raise_error('ValueError', 'inner error')\n    except TypeError:\n        assert False, 'TypeError should not catch ValueError'\nexcept ValueError:\n    caught_outer = True\nassert caught_outer, 'ValueError caught by outer handler'\n\n# === Exception in expression with multiple ext calls ===\n\n# First ext call raises, second should not be called\ntry:\n    x = raise_error('ValueError', 'first') + add_ints(1, 2)\n    assert False, 'should not reach here'\nexcept ValueError:\n    pass  # Expected\n\n# === External exception in try body with finally ===\n\nfinally_ran = False\ntry:\n    raise_error('ValueError', 'in try')\nexcept ValueError:\n    pass  # Caught\nfinally:\n    finally_ran = True\nassert finally_ran, 'finally ran after external exception caught'\n\n# External exception propagating through finally\nouter_caught = False\nfinally_ran2 = False\ntry:\n    try:\n        raise_error('KeyError', 'will propagate')\n    except ValueError:\n        assert False, 'ValueError should not catch KeyError'\n    finally:\n        finally_ran2 = True\nexcept KeyError:\n    outer_caught = True\nassert finally_ran2, 'finally ran before exception propagated'\nassert outer_caught, 'exception propagated after finally'\n\n# === Mix of normal returns and exceptions ===\n\n# Normal return, then exception\nvalue1 = add_ints(10, 20)\nassert value1 == 30, 'first ext call returned normally'\ntry:\n    raise_error('ValueError', 'after success')\n    assert False, 'should not reach here'\nexcept ValueError:\n    pass  # Expected\n\n# Exception, then normal return (after catching)\ncaught_exc = False\ntry:\n    raise_error('TypeError', 'will be caught')\nexcept TypeError:\n    caught_exc = True\nvalue2 = add_ints(5, 5)\nassert caught_exc, 'exception was caught'\nassert value2 == 10, 'ext call after caught exception returned normally'\n\n# === Exception in except handler from external function ===\n\nouter_catch = False\ntry:\n    try:\n        raise ValueError('inner')\n    except ValueError:\n        raise_error('TypeError', 'from handler')\nexcept TypeError:\n    outer_catch = True\nassert outer_catch, 'exception from handler caught by outer'\n\n# === Exception in else block from external function ===\n\nelse_exc_caught = False\ntry:\n    try:\n        pass  # No exception\n    except:\n        assert False, 'should not reach except'\n    else:\n        raise_error('RuntimeError', 'from else')\nexcept RuntimeError:\n    else_exc_caught = True\nassert else_exc_caught, 'exception from else block caught'\n\n# === Exception in finally block ===\n\n# Note: exception in finally replaces any pending exception\nfinally_exc_caught = False\ntry:\n    try:\n        pass\n    finally:\n        raise_error('ValueError', 'from finally')\nexcept ValueError:\n    finally_exc_caught = True\nassert finally_exc_caught, 'exception from finally caught'\n\n# === Nested try blocks with external exceptions ===\n\ninner_handled = False\nouter_handled = False\nfinally_count = 0\ntry:\n    try:\n        raise_error('ValueError', 'inner error')\n    except ValueError:\n        inner_handled = True\n        raise_error('TypeError', 'from inner handler')\n    finally:\n        finally_count += 1\nexcept TypeError:\n    outer_handled = True\nfinally:\n    finally_count += 1\n\nassert inner_handled, 'inner exception was handled'\nassert outer_handled, 'exception from inner handler was caught by outer'\nassert finally_count == 2, 'both finally blocks ran'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__for.py",
    "content": "# call-external\n# === External calls in for loops ===\n\n# Ext call in loop body\ntotal = 0\nfor i in range(3):\n    total = add_ints(total, 1)\nassert total == 3, f'ext call accumulator in loop, {total=}'\n\n# Ext call with loop variable\nsum_val = 0\nfor i in range(4):\n    sum_val = add_ints(sum_val, i)\nassert sum_val == 6, 'ext call with loop var'\n\n# Multiple ext calls per iteration\nresult = 0\nfor i in range(3):\n    result = add_ints(result, add_ints(i, i))\nassert result == 6, 'nested ext calls in loop'\n\n# Building list with ext calls\nitems = []\nfor i in range(3):\n    items.append(add_ints(i, 10))\nassert items[0] == 10, 'ext call list build first'\nassert items[1] == 11, 'ext call list build second'\nassert items[2] == 12, 'ext call list build third'\n\n# Chained ext calls in loop\nacc = 0\nfor i in range(3):\n    acc = add_ints(acc, 1) + add_ints(0, 1)\nassert acc == 6, 'chained ext calls in loop body'\n\n# Nested loops with ext calls\nmatrix_sum = 0\nfor i in range(2):\n    for j in range(2):\n        matrix_sum = add_ints(matrix_sum, add_ints(i, j))\nassert matrix_sum == 4, 'ext calls in nested loops'\n\n# === More nested loop edge cases ===\n\n# Nested loops building a result list\nresults = []\nfor i in range(2):\n    for j in range(2):\n        results.append(concat_strings(return_value(str(i)), return_value(str(j))))\nassert results[0] == '00', 'nested loops result list first'\nassert results[1] == '01', 'nested loops result list second'\nassert results[2] == '10', 'nested loops result list third'\nassert results[3] == '11', 'nested loops result list fourth'\n\n# If inside for loop with external call condition\nfiltered = []\nfor i in range(3):\n    if return_value(i) == i:\n        filtered.append(i)\nassert filtered == [0, 1, 2], 'if inside for with ext condition'\n\n# If inside for loop - some iterations match\nresults2 = []\nfor i in range(4):\n    # Only append even numbers (using modulo check with add_ints)\n    if add_ints(i, 0) % 2 == 0:\n        results2.append(return_value(i))\nassert results2 == [0, 2], 'if inside for filtering with ext'\n\n# Nested for with different ranges\nouter_sum = 0\nfor i in range(2):\n    inner_sum = 0\n    for j in range(3):\n        inner_sum = add_ints(inner_sum, add_ints(i, j))\n    outer_sum = add_ints(outer_sum, inner_sum)\n# i=0: (0+0)+(0+1)+(0+2) = 0+1+2 = 3\n# i=1: (1+0)+(1+1)+(1+2) = 1+2+3 = 6\n# total = 3 + 6 = 9\nassert outer_sum == 9, 'nested for with accumulator'\n\n# Three levels of nested loops\ncount = 0\nfor i in range(2):\n    for j in range(2):\n        for k in range(2):\n            count = add_ints(count, 1)\nassert count == 8, 'triple nested for with ext call'\n\n# multiple ext calls in iterable\n\next_ints = []\nfor i in add_ints(1, 1), add_ints(2, 2), add_ints(3, 3):\n    ext_ints.append(i)\nassert ext_ints == [2, 4, 6], 'multiple ext calls in iterable'\n\n# ext call iterable, get_list() returns [1, 2, 3]\ntotal = 0\nfor x in get_list():\n    total = add_ints(total, x)\nassert total == 6, 'ext call iterable'\n\n# string iteration with ext call in body\nchars = []\nfor c in 'abc':\n    chars.append(return_value(c))\nassert chars == ['a', 'b', 'c'], f'string iteration with ext call: {chars}'\n\n# unicode string iteration with ext call in body\n# Tests decr() handling of multi-byte UTF-8 characters (1-4 bytes each)\nunicode_chars = []\nfor c in 'aé中😀b':  # a (1 byte), e-acute (2), chinese (3), emoji (4), b (1)\n    unicode_chars.append(return_value(c))\nassert unicode_chars == ['a', 'é', '中', '😀', 'b'], f'unicode iteration: {unicode_chars}'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__fstring.py",
    "content": "# call-external\n# External calls in f-strings\n\ns = f'result is {add_ints(10, 20)}'\nassert s == 'result is 30', 'ext call in f-string'\n\ns = f'a={add_ints(1, 2)}, b={add_ints(3, 4)}'\nassert s == 'a=3, b=7', 'multiple ext calls in f-string'\n\n# Nested external call in f-string\ns = f'nested={add_ints(add_ints(1, 2), 3)}'\nassert s == 'nested=6', 'nested ext call in f-string'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__if.py",
    "content": "# call-external\n# === External calls in if/else expressions ===\n\n# Ternary expression with ext call in condition\nresult = 'yes' if add_ints(1, 1) == 2 else 'no'\nassert result == 'yes', 'ext call in ternary condition true'\n\nresult2 = 'yes' if add_ints(1, 1) == 3 else 'no'\nassert result2 == 'no', 'ext call in ternary condition false'\n\n# Ext call in true branch\nval = add_ints(10, 20) if True else 0\nassert val == 30, 'ext call in ternary true branch'\n\n# Ext call in false branch\nval2 = 0 if False else add_ints(5, 5)\nassert val2 == 10, 'ext call in ternary false branch'\n\n# Ext calls in both branches\nval3 = add_ints(1, 2) if True else add_ints(3, 4)\nassert val3 == 3, 'ext call in both branches takes true'\n\nval4 = add_ints(1, 2) if False else add_ints(3, 4)\nassert val4 == 7, 'ext call in both branches takes false'\n\n# === If statements with external calls ===\n\n# Ext call in if condition\nx = 0\nif add_ints(1, 1) == 2:\n    x = 100\nassert x == 100, 'ext call in if statement condition true'\n\ny = 0\nif add_ints(1, 1) == 3:\n    y = 100\nassert y == 0, 'ext call in if statement condition false'\n\n# Ext call in if body\nz = 0\nif True:\n    z = add_ints(50, 50)\nassert z == 100, 'ext call in if body'\n\n# Ext call in else body\nw = 0\nif False:\n    w = 1\nelse:\n    w = add_ints(25, 75)\nassert w == 100, 'ext call in else body'\n\n# Nested ext calls in condition\nnested = 0\nif add_ints(add_ints(1, 2), add_ints(3, 4)) == 10:\n    nested = 1\nassert nested == 1, 'nested ext calls in if condition'\n\n# Chained conditions with ext calls\nresult3 = 0\nif add_ints(1, 1) == 2 and add_ints(2, 2) == 4:\n    result3 = 1\nassert result3 == 1, 'multiple ext calls in and condition'\n\nresult4 = 0\nif add_ints(1, 1) == 3 or add_ints(2, 2) == 4:\n    result4 = 1\nassert result4 == 1, 'multiple ext calls in or condition'\n\n# Comparison with ext call results\ncmp = add_ints(10, 5) > add_ints(5, 5)\nassert cmp == True, 'comparing two ext call results'\n\n# === Nested if statements with external calls ===\n\n# Nested if with ext calls in both conditions (both true)\nresult = 'none'\nif return_value(1) == 1:\n    if return_value(2) == 2:\n        result = 'inner'\n    else:\n        result = 'outer_only'\nelse:\n    result = 'failed'\nassert result == 'inner', 'nested if both conditions true'\n\n# Nested if - outer true, inner false\nresult2 = 'none'\nif return_value(1) == 1:\n    if return_value(2) == 999:\n        result2 = 'inner'\n    else:\n        result2 = 'outer_only'\nelse:\n    result2 = 'failed'\nassert result2 == 'outer_only', 'nested if inner false'\n\n# Nested if - outer false\nresult3 = 'none'\nif return_value(1) == 999:\n    if return_value(2) == 2:\n        result3 = 'inner'\n    else:\n        result3 = 'outer_only'\nelse:\n    result3 = 'xxx'\nassert result3 == 'xxx', 'nested if outer false'\n\n# Triple nested if - all true\nresult4 = 0\nif return_value(1) == 1:\n    if return_value(2) == 2:\n        if return_value(3) == 3:\n            result4 = 123\nassert result4 == 123, 'triple nested if all true'\n\n# If condition with multiple ext calls (addition)\nresult5 = 0\nif add_ints(1, 2) + add_ints(3, 4) == 10:\n    result5 = 1\nassert result5 == 1, 'if condition with multiple ext calls'\n\n# For loop inside if with external condition\ntotal = 0\nif return_value(1) == 1:\n    for i in range(3):\n        total = add_ints(total, return_value(i))\nassert total == 3, 'for loop inside if with ext condition'\n\n# For loop inside if - condition false\ntotal2 = 0\nif return_value(1) == 999:\n    for i in range(3):\n        total2 = add_ints(total2, i)\nassert total2 == 0, 'for loop inside if condition false'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__if_condition.py",
    "content": "# call-external\n# External calls in if conditions\n\n\ndef check_positive():\n    if add_ints(1, 2) > 0:\n        return 'positive'\n    return 'not positive'\n\n\nresult = check_positive()\nassert result == 'positive', 'ext call in if condition'\n\n\ndef check_with_else():\n    if add_ints(-5, 3) > 0:\n        return 'positive'\n    else:\n        return 'negative or zero'\n\n\nresult = check_with_else()\nassert result == 'negative or zero', 'ext call in if condition with else'\n\n\ndef check_elif():\n    val = add_ints(5, 5)\n    if val > 15:\n        return 'big'\n    elif val > 5:\n        return 'medium'\n    else:\n        return 'small'\n\n\nresult = check_elif()\nassert result == 'medium', 'ext call result used in elif chain'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__in_closure.py",
    "content": "# call-external\n# External function calls inside closures (nested functions with captured variables).\n\n\ndef outer_with_nested():\n    x = 10\n\n    def inner():\n        return add_ints(x, 5)\n\n    return inner()\n\n\nassert outer_with_nested() == 15, 'ext call in nested function'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__in_function.py",
    "content": "# call-external\n# === External function calls inside user-defined functions ===\n\n# Basic function calling external function\ndef add_wrapper(a, b):\n    return add_ints(a, b)\n\n\nresult = add_wrapper(10, 20)\nassert result == 30, 'basic ext call in function'\n\n\n# Function with multiple external calls (sequential)\ndef multi_ext():\n    x = add_ints(1, 2)\n    y = add_ints(3, 4)\n    return add_ints(x, y)\n\n\nassert multi_ext() == 10, 'multiple ext calls in function'\n\n\n# External call in function with local variable usage\ndef with_locals():\n    x = 100\n    y = add_ints(x, 50)\n    z = y * 2\n    return z\n\n\nassert with_locals() == 300, 'ext call with locals'\n\n\n# Function returning external call result\ndef get_sum(a, b, c):\n    temp = add_ints(a, b)\n    return add_ints(temp, c)\n\n\nassert get_sum(1, 2, 3) == 6, 'chained ext calls in function'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__in_function_simple.py",
    "content": "# call-external\ndef foo():\n    return add_ints(1, 2)\n\n\nresult = foo()\nassert result == 3, 'basic ext call in function'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__literals.py",
    "content": "# call-external\n# External calls in list and dict literals\n\n# External call in list literal\nlst = [add_ints(1, 2), add_ints(3, 4)]\nassert lst[0] == 3, 'ext call in list literal [0]'\nassert lst[1] == 7, 'ext call in list literal [1]'\n\n# External call in tuple literal\ntup = (add_ints(1, 1), add_ints(2, 2))\nassert tup[0] == 2, 'ext call in tuple literal [0]'\nassert tup[1] == 4, 'ext call in tuple literal [1]'\n\n# External call in dict value\nd = {'a': add_ints(5, 5), 'b': add_ints(10, 10)}\nassert d['a'] == 10, 'ext call in dict value a'\nassert d['b'] == 20, 'ext call in dict value b'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__multi_in_func.py",
    "content": "# call-external\n# Multiple external calls within user-defined functions\n\n\ndef compute_sum():\n    a = add_ints(1, 2)\n    b = add_ints(3, 4)\n    c = add_ints(5, 6)\n    return a + b + c\n\n\nresult = compute_sum()\nassert result == 21, 'multiple sequential ext calls in func'\n\n\ndef compute_nested():\n    return add_ints(add_ints(1, 2), add_ints(3, 4))\n\n\nresult = compute_nested()\nassert result == 10, 'nested ext calls in func'\n\n\ndef outer():\n    def inner():\n        return add_ints(10, 20)\n\n    return inner() + add_ints(1, 2)\n\n\nresult = outer()\nassert result == 33, 'ext call in nested func plus outer ext call'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__name_lookup.py",
    "content": "# call-external\n# Tests for NameLookup resolution with various value types.\n# Verifies that the host can inject non-function values (constants)\n# into the sandbox namespace via the NameLookup mechanism.\n\n# === Integer constant ===\nassert CONST_INT == 42, f'CONST_INT should be 42, got {CONST_INT}'\nassert CONST_INT + 8 == 50, 'CONST_INT arithmetic'\nassert type(CONST_INT) == int, f'CONST_INT type should be int, got {type(CONST_INT)}'\n\n# === String constant ===\nassert CONST_STR == 'hello', f'CONST_STR should be hello, got {CONST_STR}'\nassert CONST_STR + ' world' == 'hello world', 'CONST_STR concatenation'\nassert len(CONST_STR) == 5, f'CONST_STR length should be 5, got {len(CONST_STR)}'\nassert type(CONST_STR) == str, f'CONST_STR type should be str, got {type(CONST_STR)}'\n\n# === Float constant ===\nassert CONST_FLOAT == 3.14, f'CONST_FLOAT should be 3.14, got {CONST_FLOAT}'\nassert CONST_FLOAT + 0.86 == 4.0, 'CONST_FLOAT arithmetic'\nassert type(CONST_FLOAT) == float, f'CONST_FLOAT type should be float, got {type(CONST_FLOAT)}'\n\n# === Boolean constant ===\nassert CONST_BOOL == True, f'CONST_BOOL should be True, got {CONST_BOOL}'\nassert CONST_BOOL and True, 'CONST_BOOL in boolean expression'\nassert type(CONST_BOOL) == bool, f'CONST_BOOL type should be bool, got {type(CONST_BOOL)}'\n\n# === List constant ===\nassert CONST_LIST == [1, 2, 3], f'CONST_LIST should be [1, 2, 3], got {CONST_LIST}'\nassert len(CONST_LIST) == 3, f'CONST_LIST length should be 3, got {len(CONST_LIST)}'\nassert CONST_LIST[0] == 1, 'CONST_LIST first element'\nassert CONST_LIST[-1] == 3, 'CONST_LIST last element'\nassert type(CONST_LIST) == list, f'CONST_LIST type should be list, got {type(CONST_LIST)}'\n\n# === None constant ===\nassert CONST_NONE is None, f'CONST_NONE should be None, got {CONST_NONE}'\nassert type(CONST_NONE) == type(None), f'CONST_NONE type should be NoneType, got {type(CONST_NONE)}'\n\n# === Caching: same constant used twice should work ===\nx = CONST_INT\ny = CONST_INT\nassert x == y == 42, 'cached CONST_INT should be consistent'\n\n# === Mixed: constants and external functions in the same code ===\nresult = add_ints(CONST_INT, 8)\nassert result == 50, f'add_ints(CONST_INT, 8) should be 50, got {result}'\n\nstr_result = concat_strings(CONST_STR, ' world')\nassert str_result == 'hello world', f'concat with CONST_STR should be hello world, got {str_result}'\n\n# === Constants used in control flow ===\nif CONST_BOOL:\n    flag = 'yes'\nelse:\n    flag = 'no'\nassert flag == 'yes', f'CONST_BOOL in if should take true branch, got {flag}'\n\n# === Constants used in loops ===\ntotal = 0\nfor item in CONST_LIST:\n    total = total + item\nassert total == 6, f'sum of CONST_LIST should be 6, got {total}'\n\n\n# === Constants in function scope ===\ndef use_constant():\n    return CONST_INT * 2\n\n\nassert use_constant() == 84, f'CONST_INT in function should be 84, got {use_constant()}'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__name_lookup_undefined.py",
    "content": "# call-external\n# When NameLookup returns Undefined for an unknown name, NameError is raised.\ntotally_unknown_name\n# Raise=NameError(\"name 'totally_unknown_name' is not defined\")\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__nested_calls.py",
    "content": "# call-external\n# External calls in nested call expressions\n\n# Nested external calls - inner first\nresult = add_ints(add_ints(1, 2), 3)\nassert result == 6, 'nested ext calls'\n\n# Triple nested\nresult = add_ints(add_ints(add_ints(1, 1), 2), 3)\nassert result == 7, 'triple nested ext calls'\n\n# Two separate nested calls\nresult = add_ints(add_ints(1, 2), add_ints(3, 4))\nassert result == 10, 'two nested ext calls in args'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__recursion_bug.py",
    "content": "# call-external\n# BUG: External calls in recursive functions produce wrong results\n# Recursion with external calls doesn't compute the correct value\n\n\ndef sum_with_ext(n):\n    if n <= 0:\n        return 0\n    return add_ints(n, sum_with_ext(n - 1))\n\n\n# sum_with_ext(3) should compute:\n#   add_ints(3, sum_with_ext(2))\n#   add_ints(3, add_ints(2, sum_with_ext(1)))\n#   add_ints(3, add_ints(2, add_ints(1, sum_with_ext(0))))\n#   add_ints(3, add_ints(2, add_ints(1, 0)))\n#   = 3 + 2 + 1 = 6\nresult = sum_with_ext(3)\nassert result == 6, 'recursive ext call: 1+2+3=6'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__return.py",
    "content": "# call-external\n# External calls in return statements\n\n\ndef direct_return():\n    return add_ints(10, 20)\n\n\nresult = direct_return()\nassert result == 30, 'ext call as direct return value'\n\n\ndef return_with_expression():\n    return add_ints(1, 2) + add_ints(3, 4)\n\n\nresult = return_with_expression()\nassert result == 10, 'ext call expression as return value'\n\n\ndef conditional_return():\n    if return_value(True):\n        return add_ints(100, 200)\n    return add_ints(1, 1)\n\n\nresult = conditional_return()\nassert result == 300, 'ext call in conditional return'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__side_effects.py",
    "content": "# call-external\ndef log(msg):\n    print(msg)\n    return 1\n\n\ndef inner(x):\n    print('Inner called')\n    val = add_ints(x, 1)  # External call\n    print('Inner resumed')\n    return val\n\n\ndef outer():\n    print('Outer calling inner')\n    # If side effects are duplicated, we'll see \"Evaluating arg\" twice\n    res = inner(log('Evaluating arg'))\n    print('Outer returned')\n    return res\n\n\nprint('Starting')\nres = outer()\nprint(f'Result: {res}')\nassert res == 2\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__subscript.py",
    "content": "# call-external\n# External calls in subscript operations\n\n# External call as subscript index\nitems = [10, 20, 30]\nresult = items[add_ints(0, 1)]\nassert result == 20, 'ext call as subscript index'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__ternary.py",
    "content": "# call-external\n# External calls in ternary expressions (if/else expressions)\n\n# External call in true branch\nresult = add_ints(1, 2) if True else add_ints(10, 20)\nassert result == 3, 'ext call in ternary true branch'\n\n# External call in false branch\nresult = add_ints(1, 2) if False else add_ints(10, 20)\nassert result == 30, 'ext call in ternary false branch'\n\n# External call in condition\nresult = 'yes' if return_value(True) else 'no'\nassert result == 'yes', 'ext call in ternary condition (true)'\n\nresult = 'yes' if return_value(False) else 'no'\nassert result == 'no', 'ext call in ternary condition (false)'\n\n# External calls in both branches\nresult = add_ints(1, 2) if return_value(True) else add_ints(10, 20)\nassert result == 3, 'ext call in condition and true branch'\n\nresult = add_ints(1, 2) if return_value(False) else add_ints(10, 20)\nassert result == 30, 'ext call in condition and false branch'\n\n# Nested ternary with external calls\nresult = add_ints(1, 1) if return_value(True) else (add_ints(2, 2) if return_value(False) else add_ints(3, 3))\nassert result == 2, 'nested ternary with ext calls'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__try.py",
    "content": "# call-external\n# === External calls in try blocks ===\n\n# Basic external call in try body\nresult = None\ntry:\n    result = add_ints(10, 20)\nexcept:\n    result = -1\nassert result == 30, 'ext call in try body'\n\n# Multiple external calls in try body\ntry:\n    a = add_ints(1, 2)\n    b = add_ints(3, 4)\n    c = add_ints(a, b)\nexcept:\n    c = -1\nassert c == 10, 'multiple ext calls in try body'\n\n# Nested external calls in try body\ntry:\n    nested = add_ints(add_ints(1, 2), add_ints(3, 4))\nexcept:\n    nested = -1\nassert nested == 10, 'nested ext calls in try body'\n\n# === External calls in except blocks ===\n\n# External call in except handler\nhandler_result = None\ntry:\n    raise ValueError('error')\nexcept ValueError:\n    handler_result = add_ints(100, 200)\nassert handler_result == 300, 'ext call in except handler'\n\n# Multiple external calls in except handler\ntry:\n    raise TypeError('error')\nexcept TypeError:\n    x = add_ints(5, 5)\n    y = add_ints(10, 10)\n    handler_sum = add_ints(x, y)\nassert handler_sum == 30, 'multiple ext calls in except handler'\n\n# External call with exception variable\nexc_with_ext = None\ntry:\n    raise ValueError('test')\nexcept ValueError as e:\n    prefix = concat_strings('caught: ', repr(e))\n    exc_with_ext = prefix\nassert exc_with_ext == \"caught: ValueError('test')\", 'ext call with exception variable'\n\n# === External calls in else blocks ===\n\n# External call in else block\nelse_result = None\ntry:\n    x = 1  # No exception\nexcept:\n    else_result = -1\nelse:\n    else_result = add_ints(50, 50)\nassert else_result == 100, 'ext call in else block'\n\n# Multiple external calls in else block\ntry:\n    pass\nexcept:\n    else_multi = -1\nelse:\n    p = add_ints(1, 2)\n    q = add_ints(3, 4)\n    else_multi = add_ints(p, q)\nassert else_multi == 10, 'multiple ext calls in else block'\n\n# === External calls in finally blocks ===\n\n# External call in finally block\nfinally_result = None\ntry:\n    x = 1\nfinally:\n    finally_result = add_ints(25, 75)\nassert finally_result == 100, 'ext call in finally block'\n\n# Finally with external call after exception caught\nfinally_after_exc = None\ntry:\n    raise ValueError('error')\nexcept ValueError:\n    pass\nfinally:\n    finally_after_exc = add_ints(1, 99)\nassert finally_after_exc == 100, 'ext call in finally after caught exception'\n\n# Multiple external calls in finally\ntry:\n    pass\nfinally:\n    f1 = add_ints(10, 20)\n    f2 = add_ints(30, 40)\n    finally_multi = add_ints(f1, f2)\nassert finally_multi == 100, 'multiple ext calls in finally block'\n\n# === External calls across multiple phases ===\n\n# External calls in try, except, and finally\nall_phases = []\ntry:\n    all_phases.append(add_ints(1, 0))  # 1\n    raise ValueError('error')\nexcept ValueError:\n    all_phases.append(add_ints(2, 0))  # 2\nfinally:\n    all_phases.append(add_ints(3, 0))  # 3\nassert all_phases == [1, 2, 3], 'ext calls in try, except, and finally'\n\n# External calls in try, else, and finally (no exception)\nno_exc_phases = []\ntry:\n    no_exc_phases.append(add_ints(10, 0))  # 10\nexcept:\n    no_exc_phases.append(-1)\nelse:\n    no_exc_phases.append(add_ints(20, 0))  # 20\nfinally:\n    no_exc_phases.append(add_ints(30, 0))  # 30\nassert no_exc_phases == [10, 20, 30], 'ext calls in try, else, and finally'\n\n# === External calls in nested try blocks ===\n\n# Nested try with external calls at each level\nouter_val = None\ninner_val = None\ntry:\n    outer_val = add_ints(100, 0)\n    try:\n        inner_val = add_ints(200, 0)\n        raise ValueError('inner')\n    except ValueError:\n        inner_val = add_ints(inner_val, 50)\nexcept:\n    outer_val = -1\nassert outer_val == 100, 'ext call in outer try'\nassert inner_val == 250, 'ext call in inner try and handler'\n\n# === External calls in exception type expression ===\n# (Exception type is evaluated at handler matching time)\n\n# External call producing value used after try\npost_try = None\ntry:\n    pre = add_ints(5, 5)\nexcept:\n    pre = -1\npost_try = add_ints(pre, 10)\nassert post_try == 20, 'ext call result used after try block'\n\n# === External call in finally with unhandled exception ===\n# Finally should still run even when exception propagates\nfinally_with_propagate = None\ntry:\n    try:\n        finally_with_propagate = add_ints(0, 0)  # Initialize\n        raise KeyError('unhandled')\n    except ValueError:\n        pass  # Won't catch KeyError\n    finally:\n        finally_with_propagate = add_ints(42, 0)  # Should still run\nexcept KeyError:\n    pass  # Catch propagated exception\nassert finally_with_propagate == 42, 'ext call in finally should run even with unhandled exception'\n\n# === External call in except handler that then raises ===\nhandler_before_raise = None\ntry:\n    try:\n        raise ValueError('original')\n    except ValueError:\n        handler_before_raise = add_ints(10, 0)  # External call before raising\n        raise TypeError('from handler')\nexcept TypeError:\n    pass\nassert handler_before_raise == 10, 'ext call in handler before raising'\n\n# === External call in else block that then raises ===\nelse_before_raise = None\ntry:\n    try:\n        pass  # No exception\n    except:\n        pass\n    else:\n        else_before_raise = add_ints(20, 0)  # External call before raising\n        raise ValueError('from else')\nexcept ValueError:\n    pass\nassert else_before_raise == 20, 'ext call in else before raising'\n\n# === External call preserves state across try/except ===\nstate_before = add_ints(1000, 0)\nstate_after = None\ntry:\n    state_after = add_ints(state_before, 1)\n    raise ValueError('test')\nexcept ValueError:\n    state_after = add_ints(state_after, 10)\nfinally:\n    state_after = add_ints(state_after, 100)\nassert state_after == 1111, 'state preserved across try/except with ext calls'\n\n# === Multiple except handlers with external calls ===\nwhich_handler = None\ntry:\n    raise TypeError('test')\nexcept ValueError:\n    which_handler = add_ints(1, 0)\nexcept TypeError:\n    which_handler = add_ints(2, 0)\nexcept KeyError:\n    which_handler = add_ints(3, 0)\nassert which_handler == 2, 'ext call in correct handler with multiple handlers'\n\n# === External call in finally with pending exception (after handler raises) ===\nfinally_after_handler_raise = None\ntry:\n    try:\n        raise ValueError('original')\n    except ValueError:\n        finally_after_handler_raise = add_ints(10, 0)  # External call before raising\n        raise TypeError('from handler')\n    finally:\n        # This external call should work even though there's a pending exception\n        finally_after_handler_raise = add_ints(finally_after_handler_raise, 5)\nexcept TypeError:\n    pass\nassert finally_after_handler_raise == 15, 'ext call in finally with pending exception from handler'\n\n# === External call in finally with pending exception (no matching handler) ===\nfinally_with_pending_exc = None\ntry:\n    try:\n        finally_with_pending_exc = add_ints(0, 0)\n        raise KeyError('no handler')\n    except ValueError:\n        pass  # Won't catch KeyError\n    finally:\n        # This external call should work even though KeyError is pending\n        finally_with_pending_exc = add_ints(100, 0)\nexcept KeyError:\n    pass  # Catch it here\nassert finally_with_pending_exc == 100, 'ext call in finally with unhandled exception pending'\n\n# === External call in finally with return (uses simple values) ===\n# Note: External calls in user-defined functions are not supported,\n# so we test pending return with built-in operations only\nfinally_return_result = None\ntry:\n    finally_return_result = 'in_try'\nfinally:\n    pass  # finally runs but doesn't override\nassert finally_return_result == 'in_try', 'finally runs with pending value'\n\n# === Multiple external calls in finally with pending exception ===\nmulti_finally = None\ntry:\n    try:\n        raise ValueError('test')\n    except TypeError:\n        pass  # Won't match\n    finally:\n        a = add_ints(1, 2)\n        b = add_ints(3, 4)\n        multi_finally = add_ints(a, b)\nexcept ValueError:\n    pass\nassert multi_finally == 10, 'multiple ext calls in finally with pending exception'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__try_simple.py",
    "content": "# call-external\n# Test external call with exception variable\n\nexc_with_ext = None\ntry:\n    raise ValueError('test')\nexcept ValueError as e:\n    prefix = concat_strings('caught: ', repr(e))\n    exc_with_ext = prefix\nassert exc_with_ext == \"caught: ValueError('test')\", 'ext call with exception variable'\n"
  },
  {
    "path": "crates/monty/test_cases/ext_call__unary.py",
    "content": "# call-external\n# External calls in unary expressions\n\n# Negation of external call result\nresult = -add_ints(3, 4)\nassert result == -7, 'negation of ext call'\n\n# Not of external call\nresult = not return_value(False)\nassert result == True, 'not of ext call returning False'\n\nresult = not return_value(True)\nassert result == False, 'not of ext call returning True'\n"
  },
  {
    "path": "crates/monty/test_cases/frozenset__ops.py",
    "content": "# === Construction ===\nfs = frozenset()\nassert len(fs) == 0, 'empty frozenset len'\nassert fs == frozenset(), 'empty frozenset equality'\n\nfs = frozenset([1, 2, 3])\nassert len(fs) == 3, 'frozenset from list len'\n\n# === Copy ===\nfs = frozenset([1, 2, 3])\nfs2 = fs.copy()\nassert fs == fs2, 'copy equality'\n\n# === Union ===\nfs1 = frozenset([1, 2])\nfs2 = frozenset([2, 3])\nu = fs1.union(fs2)\nassert len(u) == 3, 'union len'\n\n# === Intersection ===\nfs1 = frozenset([1, 2, 3])\nfs2 = frozenset([2, 3, 4])\ni = fs1.intersection(fs2)\nassert len(i) == 2, 'intersection len'\n\n# === Difference ===\nfs1 = frozenset([1, 2, 3])\nfs2 = frozenset([2, 3, 4])\nd = fs1.difference(fs2)\nassert len(d) == 1, 'difference len'\n\n# === Symmetric Difference ===\nfs1 = frozenset([1, 2, 3])\nfs2 = frozenset([2, 3, 4])\nsd = fs1.symmetric_difference(fs2)\nassert len(sd) == 2, 'symmetric_difference len'\n\n# === Binary operators ===\nfs = frozenset([1, 2])\nother_fs = frozenset([2, 3])\ns = {2, 3}\n\nassert fs & other_fs == frozenset([2]), 'frozenset & frozenset works'\nassert fs | other_fs == frozenset([1, 2, 3]), 'frozenset | frozenset works'\nassert fs ^ other_fs == frozenset([1, 3]), 'frozenset ^ frozenset works'\nassert fs - other_fs == frozenset([1]), 'frozenset - frozenset works'\n\nassert fs & s == frozenset([2]), 'frozenset & set works'\nassert fs | s == frozenset([1, 2, 3]), 'frozenset | set works'\nassert fs ^ s == frozenset([1, 3]), 'frozenset ^ set works'\nassert fs - s == frozenset([1]), 'frozenset - set works'\n\nkeys = {'a': 1, 'b': 2}.keys()\nitems = {'a': 1, 'b': 2}.items()\nassert frozenset({'a'}) & keys == frozenset({'a'}), 'frozenset & dict_keys works'\nassert frozenset({'a'}) | keys == frozenset({'a', 'b'}), 'frozenset | dict_keys works'\nassert frozenset({('a', 1)}) ^ items == frozenset({('b', 2)}), 'frozenset ^ dict_items works'\nassert frozenset({('a', 1), ('b', 2)}) - items == frozenset(), 'frozenset - dict_items works'\n\nassert type(fs | s).__name__ == 'frozenset', 'frozenset operators keep the left operand type'\n\ntry:\n    fs & [1, 2]\n    assert False, 'frozenset operators reject non-set rhs'\nexcept TypeError as e:\n    assert str(e) == \"unsupported operand type(s) for &: 'frozenset' and 'list'\", (\n        'frozenset & rhs error matches CPython'\n    )\n\n# === Issubset ===\nfs1 = frozenset([1, 2])\nfs2 = frozenset([1, 2, 3])\nassert fs1.issubset(fs2) == True, 'issubset true'\nassert fs2.issubset(fs1) == False, 'issubset false'\n\n# === Issuperset ===\nfs1 = frozenset([1, 2, 3])\nfs2 = frozenset([1, 2])\nassert fs1.issuperset(fs2) == True, 'issuperset true'\nassert fs2.issuperset(fs1) == False, 'issuperset false'\n\n# === Isdisjoint ===\nfs1 = frozenset([1, 2])\nfs2 = frozenset([3, 4])\nfs3 = frozenset([2, 3])\nassert fs1.isdisjoint(fs2) == True, 'isdisjoint true'\nassert fs1.isdisjoint(fs3) == False, 'isdisjoint false'\n\n# === Bool ===\nassert bool(frozenset()) == False, 'empty frozenset is falsy'\nassert bool(frozenset([1])) == True, 'non-empty frozenset is truthy'\n\n# === repr ===\nassert repr(frozenset()) == 'frozenset()', 'empty frozenset repr'\n\n# === Hashing ===\nfs = frozenset([1, 2, 3])\nh = hash(fs)\nassert isinstance(h, int), 'frozenset hash is int'\n\n# Same elements should have same hash\nfs1 = frozenset([1, 2, 3])\nfs2 = frozenset([3, 2, 1])  # Different order\nassert hash(fs1) == hash(fs2), 'frozenset hash is order-independent'\n\n# === As dict key ===\nd = {}\nfs = frozenset([1, 2])\nd[fs] = 'value'\nassert d[fs] == 'value', 'frozenset as dict key'\nassert d[frozenset([2, 1])] == 'value', 'frozenset key lookup order-independent'\n\n# === Construction from various iterables ===\nfs = frozenset('abc')\nassert len(fs) == 3, 'frozenset from string len'\nassert 'a' in fs and 'b' in fs and 'c' in fs, 'frozenset from string elements'\n\nfs = frozenset((1, 2, 3))\nassert fs == frozenset({1, 2, 3}), 'frozenset from tuple'\n\nfs = frozenset(range(5))\nassert fs == frozenset({0, 1, 2, 3, 4}), 'frozenset from range'\n\nfs = frozenset({1, 2, 3})\nassert len(fs) == 3, 'frozenset from set'\n\n# === Containment (in / not in) ===\nfs = frozenset({1, 2, 3})\nassert 1 in fs, 'in frozenset positive'\nassert 4 not in fs, 'not in frozenset'\nassert 'x' not in frozenset({'a', 'b'}), 'not in frozenset strings'\n\n# === Iteration ===\nresult = []\nfor x in frozenset({1, 2, 3}):\n    result.append(x)\nassert len(result) == 3, 'frozenset iteration length'\nassert set(result) == {1, 2, 3}, 'frozenset iteration elements'\n\nresult = []\nfor x in frozenset():\n    result.append(x)\nassert result == [], 'empty frozenset iteration'\n\n# === Inequality (!=) ===\nassert frozenset({1, 2}) != frozenset({1, 3}), 'frozenset ne different'\nassert not (frozenset({1, 2}) != frozenset({1, 2})), 'frozenset ne same'\n\n# === Methods accepting iterables ===\nassert frozenset({1, 2}).union([3, 4]) == frozenset({1, 2, 3, 4}), 'union with list arg'\nassert frozenset({1, 2, 3}).intersection([2, 3, 4]) == frozenset({2, 3}), 'intersection with list arg'\nassert frozenset({1, 2, 3}).difference([2]) == frozenset({1, 3}), 'difference with list arg'\nassert frozenset({1, 2}).symmetric_difference([2, 3]) == frozenset({1, 3}), 'symmetric_difference with list arg'\nassert frozenset({1}).union(range(3)) == frozenset({0, 1, 2}), 'union with range arg'\nassert frozenset({1}).union((2, 3)) == frozenset({1, 2, 3}), 'union with tuple arg'\n\n# === issubset/issuperset/isdisjoint with non-set iterables ===\nfs = frozenset({1, 2, 3})\nassert fs.issubset(range(10)), 'issubset with range'\nassert fs.issuperset([1, 2]), 'issuperset with list'\nassert fs.isdisjoint([4, 5, 6]), 'isdisjoint with list'\nassert not fs.isdisjoint([3, 4]), 'not isdisjoint with list'\n\n# === Different hashes for different frozensets ===\nfs1 = frozenset({1, 2})\nfs2 = frozenset({3, 4})\n# Not guaranteed to be different, but very likely\n# Instead just verify they're integers and stable\nassert hash(fs1) == hash(frozenset({2, 1})), 'hash stable across order'\nassert hash(frozenset()) == hash(frozenset()), 'empty frozenset hash stable'\n\n# === Frozenset as set element ===\ns = {frozenset({1, 2}), frozenset({3, 4})}\nassert len(s) == 2, 'set of frozensets'\nassert frozenset({1, 2}) in s, 'frozenset element lookup'\n# Duplicate frozenset should dedup\ns2 = {frozenset({1}), frozenset({1})}\nassert len(s2) == 1, 'duplicate frozensets dedup in set'\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__all.py",
    "content": "# === Basic f-strings ===\nassert f'hello' == 'hello', 'basic f-string'\nassert f'' == '', 'empty f-string'\n\n# === Simple interpolation ===\nx = 'world'\nassert f'hello {x}' == 'hello world', 'simple interpolation'\n\n# multiple interpolations\na = 1\nb = 2\nassert f'{a} + {b} = {a + b}' == '1 + 2 = 3', 'multiple interpolations'\n\n# expression in f-string\nassert f'{1 + 2 + 3}' == '6', 'expression'\n\n# === Value types ===\n# list value\nx = [1, 2, 3]\nassert f'list: {x}' == 'list: [1, 2, 3]', 'list value'\n\n# bool value\nx = True\nassert f'value: {x}' == 'value: True', 'bool value'\n\n# int value\nassert f'{42}' == '42', 'int value'\n\n# float value\nassert f'{3.14}' == '3.14', 'float value'\n\n# None value\nassert f'{None}' == 'None', 'None value'\n\n# === Conversion flags (!s, !r, !a) ===\n# conversion !s (str)\nassert f'{42!s}' == '42', 'conversion !s'\n\n# conversion !r (repr)\nassert f'{\"hello\"!r}' == \"'hello'\", 'conversion !r'\n\n# conversion !r on int (should be same as str for int)\nassert f'{42!r}' == '42', 'conversion !r on int'\n\n# conversion !r on list\nassert f'{[1, 2]!r}' == '[1, 2]', 'conversion !r on list'\n\n# conversion !s on string (no quotes)\nassert f'{\"hello\"!s}' == 'hello', 'conversion !s on string'\n\n# conversion !a (ascii) - escapes non-ASCII characters\nassert f'{\"café\"!a}' == \"'caf\\\\xe9'\", 'conversion !a'\nassert f'{\"hello\"!a}' == \"'hello'\", 'conversion !a ascii only'\nassert f'{\"日本\"!a}' == \"'\\\\u65e5\\\\u672c'\", 'conversion !a unicode'\n\n# === String padding and alignment ===\n# format spec: width (left-aligned by default for strings)\nassert f'{\"hi\":10}' == 'hi        ', 'format width'\n\n# format spec: left align\nassert f'{\"hi\":<10}' == 'hi        ', 'format left align'\n\n# format spec: right align\nassert f'{\"hi\":>10}' == '        hi', 'format right align'\n\n# format spec: center align\nassert f'{\"hi\":^10}' == '    hi    ', 'format center align'\n\n# center align with odd padding\nassert f'{\"zip\":^6}' == ' zip  ', 'format center align odd'\n\n# format spec: fill character\nassert f'{\"hi\":*>10}' == '********hi', 'format fill right'\nassert f'{\"hi\":_<10}' == 'hi________', 'format fill left'\nassert f'{\"hi\":*^10}' == '****hi****', 'format fill center'\n\n# string truncation with precision\nassert f'{\"xylophone\":.5}' == 'xylop', 'string truncation'\nassert f'{\"xylophone\":10.5}' == 'xylop     ', 'string truncation with width'\n\n# === Integer formatting ===\n# basic integer\nassert f'{42}' == '42', 'basic integer'\n\n# integer with :d type\nassert f'{42:d}' == '42', 'integer :d'\n\n# integer padding\nassert f'{42:4d}' == '  42', 'integer padding'\nassert f'{42:04d}' == '0042', 'integer zero padding'\n\n# integer with sign\nassert f'{42:+d}' == '+42', 'integer positive sign'\nassert f'{42: d}' == ' 42', 'integer space for positive'\nassert f'{-42:+d}' == '-42', 'integer negative with sign'\nassert f'{-42: d}' == '-42', 'integer negative space'\n\n# sign-aware padding\nassert f'{-23:=5d}' == '-  23', 'sign-aware padding'\n\n# === Float formatting ===\n# basic float\nassert f'{3.14159}' == '3.14159', 'basic float'\n\n# float with :f type\nassert f'{3.141592653589793:f}' == '3.141593', 'float :f'\n\n# float precision\nassert f'{3.141592653589793:.2f}' == '3.14', 'float precision'\nassert f'{3.141592653589793:.4f}' == '3.1416', 'float precision 4'\n\n# float width and precision\nassert f'{3.141592653589793:06.2f}' == '003.14', 'float zero pad with precision'\nassert f'{3.141592653589793:10.2f}' == '      3.14', 'float width with precision'\n\n# float with sign\nassert f'{3.14:+.2f}' == '+3.14', 'float positive sign'\nassert f'{-3.14:+.2f}' == '-3.14', 'float negative with sign'\nassert f'{3.14:-.2f}' == '3.14', 'float explicit minus sign'\nassert f'{-3.14:-.2f}' == '-3.14', 'float explicit minus sign negative'\n\n# exponential notation\nassert f'{1234.5678:e}' == '1.234568e+03', 'exponential lowercase'\nassert f'{1234.5678:E}' == '1.234568E+03', 'exponential uppercase'\nassert f'{1234.5678:.2e}' == '1.23e+03', 'exponential with precision'\nassert f'{0.00012345:.2e}' == '1.23e-04', 'exponential small number'\n\n# general format (g/G) - uses exponential for very large/small numbers\nassert f'{1.5:g}' == '1.5', 'general format simple'\nassert f'{1.500:g}' == '1.5', 'general format strips trailing zeros'\nassert f'{1234567890:g}' == '1.23457e+09', 'general format large number'\n\n# percentage\nassert f'{0.25:%}' == '25.000000%', 'percentage default precision'\nassert f'{0.25:.1%}' == '25.0%', 'percentage with precision'\nassert f'{0.125:.0%}' == '12%', 'percentage zero precision'\n\n# === Nested format specs ===\nwidth = 10\nassert f'{\"hi\":{width}}' == 'hi        ', 'nested format spec width'\n\n# nested alignment and width\nalign = '^'\nassert f'{\"test\":{align}{width}}' == '   test   ', 'nested align and width'\n\n# nested precision\nprec = 3\nassert f'{\"xylophone\":.{prec}}' == 'xyl', 'nested precision'\n\n\n# === f-string in function ===\ndef greet(name):\n    return f'Hello, {name}!'\n\n\nassert greet('World') == 'Hello, World!', 'f-string in function'\n\n\n# function returning formatted value\ndef format_num(n, w):\n    return f'{n:>{w}}'\n\n\nassert format_num('x', 5) == '    x', 'f-string with params'\n\n# === Escaping ===\n# double braces to escape\nassert f'{{}}' == '{}', 'escaped braces'\nassert f'{{x}}' == '{x}', 'escaped braces with content'\nassert f'{{{42}}}' == '{42}', 'value inside escaped braces'\n\n# === Complex expressions ===\n# TODO: method call on literal - parser doesn't support this yet\n# assert f'{\"hello\".upper()}' == 'HELLO', 'method call on literal'\n\n# TODO: method call on variable - str.upper() not implemented yet\n# s = 'hello'\n# assert f'{s.upper()}' == 'HELLO', 'method call on variable'\n\n# subscript in f-string\nlst = [10, 20, 30]\nassert f'{lst[1]}' == '20', 'subscript'\n\n# dict lookup\nd = {'a': 1, 'b': 2}\nassert f'{d[\"a\"]}' == '1', 'dict lookup'\n\n# TODO: conditional expression - parser doesn't support IfExp yet\n# x = 5\n# assert f'{x if x > 0 else -x}' == '5', 'conditional positive'\n# x = -5\n# assert f'{-x if x < 0 else x}' == '5', 'conditional negative'\n\n# === String concatenation ===\nname = 'world'\n# regular string + f-string (implicit concatenation)\nassert f'hello {name}' == 'hello world', 'str concat with fstring'\n\n# === Empty interpolation expression ===\n# (this should be a syntax error, but test current behavior)\n# assert f'{}' would be syntax error\n\n# === Whitespace in format spec ===\n# no extra whitespace handling needed, width handles it\nassert f'{\"x\":5}' == 'x    ', 'single char width'\n\n# === Unicode character counting in padding ===\nx = 'café'\nassert f'{x:_<10}' == 'café______'\nassert f'{x:_>10}' == '______café'\nassert f'{x:_^10}' == '___café___'\nassert f'{x:_^11}' == '___café____'\nassert f'{x:é<10}' == 'cafééééééé'\nassert f'{x:é>10}' == 'éééééécafé'\nassert f'{x:é^10}' == 'ééécaféééé'\nassert f'{x:é^11}' == 'ééécafééééé'\n\n# === Conversion flag with type spec ===\n# conversion flag produces string, so 's' format should work\nassert f'{42!r:s}' == '42', 'conversion with type spec'\n\n# === Zero-padding with negative numbers ===\n# zero-padding should use sign-aware alignment\nx = -42\nassert f'{x:05d}' == '-0042', 'zero pad negative'\n\n# === Debug/self-documenting expressions (=) ===\na = 42\nassert f'{a=}' == 'a=42', 'basic debug expression'\nassert f'{a = }' == 'a = 42', 'debug with spaces'\nname = 'test'\nassert f'{name=}' == \"name='test'\", 'debug uses repr for strings'\nassert f'{name = }' == \"name = 'test'\", 'debug uses repr for strings'\nassert f'{name=!s}' == 'name=test', 'debug with !s conversion'\nassert f'{name=!r}' == \"name='test'\", 'debug with !r conversion'\nassert f'{1+1=}' == '1+1=2', 'debug with expression'\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_eq_align_on_str.py",
    "content": "# '=' alignment on string raises ValueError\nf'{\"hello\":=10}'\n# Raise=ValueError(\"'=' alignment not allowed in string format specifier\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_float_f_on_str.py",
    "content": "# float format specifier ':f' on string raises ValueError\nf'{\"hello\":f}'\n# Raise=ValueError(\"Unknown format code 'f' for object of type 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_int_d_on_float.py",
    "content": "# integer format specifier ':d' on float raises ValueError\nf'{3.14:d}'\n# Raise=ValueError(\"Unknown format code 'd' for object of type 'float'\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_int_d_on_str.py",
    "content": "# integer format specifier ':d' on string raises ValueError\nf'{\"hello\":d}'\n# Raise=ValueError(\"Unknown format code 'd' for object of type 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_invalid_spec.py",
    "content": "# xfail=cpython\n# invalid format specifier with trailing characters (detected at parse time)\nf'{1:10xyz}'\n# Raise=SyntaxError(\"Invalid format specifier '10xyz'\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_invalid_spec_dynamic.py",
    "content": "# invalid format specifier with dynamic spec\nspec = 'xyz'\nf'{1:{spec}}'\n# Raise=ValueError(\"Invalid format specifier 'xyz' for object of type 'int'\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_invalid_spec_str.py",
    "content": "# xfail=cpython\n# invalid format specifier for string (detected at parse time)\nf'{\"hello\":abc}'\n# Raise=SyntaxError(\"Invalid format specifier 'abc'\")\n"
  },
  {
    "path": "crates/monty/test_cases/fstring__error_str_s_on_int.py",
    "content": "# string format specifier ':s' on integer raises ValueError\nf'{42:s}'\n# Raise=ValueError(\"Unknown format code 's' for object of type 'int'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__call_duplicate_kwargs.py",
    "content": "def f(**kwargs):\n    pass\n\n\nf(**{'x': 1}, **{'x': 2})\n# Raise=TypeError(\"f() got multiple values for keyword argument 'x'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__call_unpack.py",
    "content": "def f(*args, **kwargs):\n    return args, kwargs\n\n\n# === Multiple *args ===\nassert f(*[1, 2], *[3, 4]) == ((1, 2, 3, 4), {}), 'multiple star args'\nassert f(0, *[1, 2], 3) == ((0, 1, 2, 3), {}), 'positional after star args'\nassert f(*[], *[1]) == ((1,), {}), 'unpack empty then non-empty'\n\n# === Multiple **kwargs ===\nassert f(**{'a': 1}, **{'b': 2}) == ((), {'a': 1, 'b': 2}), 'multiple star-star kwargs'\nassert f(**{'a': 1}, b=2) == ((), {'a': 1, 'b': 2}), 'named after star-star'\nassert f(key='before', **{'a': 1}) == ((), {'key': 'before', 'a': 1}), 'named before star-star'\n\n# === Mixed ===\nassert f(1, *[2, 3], **{'x': 4}) == ((1, 2, 3), {'x': 4}), 'mixed star and star-star'\n\n# === Builtin callable with GeneralizedCall (Callable::Builtin path) ===\n# max(*[1,2], *[3,4]) exercises the Callable::Builtin branch in compile_call GeneralizedCall\nresult = max(*[1, 2], *[3, 4])\nassert result == 4, 'builtin max with multiple *args'\n\nresult = min(*[5, 3], *[7, 1])\nassert result == 1, 'builtin min with multiple *args'\n\n# Builtin type and exception constructors should keep their public names in\n# **kwargs merge errors, not fall back to '<unknown>'.\ntry:\n    list(**1)\n    assert False, 'list with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('list() argument after ** must be a mapping, not int',), (\n        'builtin type non-mapping **kwargs error keeps type name'\n    )\n\ntry:\n    ValueError(**1)\n    assert False, 'ValueError with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('ValueError() argument after ** must be a mapping, not int',), (\n        'builtin exception non-mapping **kwargs error keeps exception name'\n    )\n\ntry:\n    list(a=1, **{'a': 2})\n    assert False, 'list with duplicate **kwargs should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"list() got multiple values for keyword argument 'a'\",), (\n        'builtin type duplicate **kwargs error keeps type name'\n    )\n\n# Builtin type constructors should also keep their public names in\n# non-mapping **kwargs errors so compiler call metadata matches CPython.\ntry:\n    bool(**1)\n    assert False, 'bool with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('bool() argument after ** must be a mapping, not int',), (\n        'bool non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    int(**1)\n    assert False, 'int with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('int() argument after ** must be a mapping, not int',), (\n        'int non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    float(**1)\n    assert False, 'float with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('float() argument after ** must be a mapping, not int',), (\n        'float non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    str(**1)\n    assert False, 'str with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('str() argument after ** must be a mapping, not int',), (\n        'str non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    bytes(**1)\n    assert False, 'bytes with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('bytes() argument after ** must be a mapping, not int',), (\n        'bytes non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    tuple(**1)\n    assert False, 'tuple with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('tuple() argument after ** must be a mapping, not int',), (\n        'tuple non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    dict(**1)\n    assert False, 'dict with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('dict() argument after ** must be a mapping, not int',), (\n        'dict non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    set(**1)\n    assert False, 'set with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('set() argument after ** must be a mapping, not int',), (\n        'set non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    frozenset(**1)\n    assert False, 'frozenset with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('frozenset() argument after ** must be a mapping, not int',), (\n        'frozenset non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    range(**1)\n    assert False, 'range with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('range() argument after ** must be a mapping, not int',), (\n        'range non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    slice(**1)\n    assert False, 'slice with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('slice() argument after ** must be a mapping, not int',), (\n        'slice non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    type(**1)\n    assert False, 'type with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('type() argument after ** must be a mapping, not int',), (\n        'type non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    property(**1)\n    assert False, 'property with non-mapping **arg should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('property() argument after ** must be a mapping, not int',), (\n        'property non-mapping **kwargs error keeps builtin type name'\n    )\n\ntry:\n    ValueError(a=1, **{'a': 2})\n    assert False, 'ValueError with duplicate **kwargs should raise TypeError'\nexcept TypeError as e:\n    assert e.args == (\"ValueError() got multiple values for keyword argument 'a'\",), (\n        'builtin exception duplicate **kwargs error keeps exception name'\n    )\n\n# === Expression-based callable with GeneralizedCall (compile_call_args path) ===\n# funcs[0](*[1,2], *[3,4]) exercises the GeneralizedCall branch in compile_call_args\nfuncs = [f]\nresult = funcs[0](*[1, 2], *[3, 4])\nassert result == ((1, 2, 3, 4), {}), 'subscript call with multiple *args'\n\nresult = funcs[0](**{'a': 1}, **{'b': 2})\nassert result == ((), {'a': 1, 'b': 2}), 'subscript call with multiple **kwargs'\n\n# === Named kwarg in GeneralizedCall (compile_generalized_call_body Named path) ===\n# f(*[1,2], *[3], x=5): two *unpacks → GeneralizedCall; x=5 is a Named kwarg.\n# This exercises the CallKwarg::Named arm in compile_generalized_call_body.\nresult = f(*[1, 2], *[3], x=5)\nassert result == ((1, 2, 3), {'x': 5}), 'named kwarg in multi-star GeneralizedCall'\n\nresult = funcs[0](*[1, 2], *[3], x=5)\nassert result == ((1, 2, 3), {'x': 5}), 'subscript call: named kwarg in GeneralizedCall'\n"
  },
  {
    "path": "crates/monty/test_cases/function__defaults.py",
    "content": "# Tests for default parameter values in function definitions\n\n# === Basic default values ===\ndef f_basic(a, b=10):\n    return a + b\n\n\nassert f_basic(1) == 11, 'default used'\nassert f_basic(1, 2) == 3, 'default overridden'\nassert f_basic(5) == 15, 'default used again'\n\n\n# === Multiple defaults ===\ndef f_multi(a=1, b=2):\n    return a + b\n\n\nassert f_multi() == 3, 'both defaults'\nassert f_multi(10) == 12, 'first provided'\nassert f_multi(10, 20) == 30, 'both provided'\n\n\n# === Mixed required and default ===\ndef f_mixed(a, b, c=3, d=4):\n    return a + b + c + d\n\n\nassert f_mixed(1, 2) == 10, 'required only'\nassert f_mixed(1, 2, 30) == 37, 'one default overridden'\nassert f_mixed(1, 2, 30, 40) == 73, 'all provided'\n\n\n# === Default with keyword args ===\ndef f_kw(a, b=10):\n    return a + b\n\n\nassert f_kw(1, b=20) == 21, 'keyword override'\nassert f_kw(a=5) == 15, 'keyword required, default used'\nassert f_kw(a=5, b=3) == 8, 'both keywords'\n\n\n# === Default expressions evaluated at definition ===\n# Test that default is evaluated once at definition time\ndef value_maker():\n    return 42\n\n\ndef f_eval(x=value_maker()):\n    return x\n\n\n# value_maker was called once at function definition time\nassert f_eval() == 42, 'first call uses cached default'\nassert f_eval() == 42, 'second call uses same default'\n\n\n# === Mutable default (Python gotcha - shared across calls) ===\ndef f_mutable(lst=[]):\n    lst.append(1)\n    return lst\n\n\nfirst_result = f_mutable()\nassert first_result == [1], 'first call'\nsecond_result = f_mutable()\nassert second_result == [1, 1], 'second call appends to same list'\nassert first_result is second_result, 'same list object'\n\n\n# === Multiple functions with separate defaults ===\ndef f_sep1(x=[]):\n    x.append('a')\n    return x\n\n\ndef f_sep2(x=[]):\n    x.append('b')\n    return x\n\n\nr1 = f_sep1()\nr2 = f_sep2()\nassert r1 == ['a'], 'f_sep1 default'\nassert r2 == ['b'], 'f_sep2 default'\nassert r1 is not r2, 'separate default lists'\n\n\n# === Default referencing earlier param (not supported, different test) ===\n\n\n# === Closure with defaults ===\ndef make_adder(n):\n    def add(x, y=n):\n        return x + y\n\n    return add\n\n\nadd5 = make_adder(5)\nassert add5(10) == 15, 'closure default from enclosing scope'\nassert add5(10, 3) == 13, 'closure default overridden'\n\nadd10 = make_adder(10)\nassert add10(1) == 11, 'different closure, different captured default'\n\n# Verify the two closures have independent defaults\nassert add5(1) == 6, 'add5 still uses 5'\n\n\n# === Keyword-only defaults interleaved ===\ndef kwonly_mix(*, head=1, mid, tail=3):\n    return head, mid, tail\n\n\nassert kwonly_mix(mid=2) == (1, 2, 3), 'kw-only defaults applied per parameter'\nassert kwonly_mix(head=5, mid=7) == (5, 7, 3), 'kw-only default overridden independently'\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_duplicate_arg.py",
    "content": "# Test: same argument passed both positionally and by keyword\ndef f(a, b, c):\n    return a + b + c\n\n\nf(1, 2, b=3)\n# Raise=TypeError(\"f() got multiple values for argument 'b'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_duplicate_first_arg.py",
    "content": "# Test: first argument passed both positionally and by keyword\ndef f(a, b):\n    return a + b\n\n\nf(1, a=2)\n# Raise=TypeError(\"f() got multiple values for argument 'a'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_duplicate_kwarg_cleanup.py",
    "content": "# Test that heap values are properly cleaned up when duplicate kwarg error occurs\ndef f(a, b):\n    return a\n\n\n# The list [1, 2, 3] should be cleaned up when the error occurs\n# because 'a' is passed both positionally and by keyword\nf([1, 2, 3], a=[4, 5])\n# Raise=TypeError(\"f() got multiple values for argument 'a'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_kwonly_as_positional.py",
    "content": "# Test: keyword-only argument passed positionally\ndef f(*, a, b):\n    return a + b\n\n\nf(1, 2)\n# Raise=TypeError('f() takes 0 positional arguments but 2 were given')\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_missing_all_posonly.py",
    "content": "# Test: missing all positional-only arguments\ndef f(a, b, /):\n    return a + b\n\n\nf()\n# Raise=TypeError(\"f() missing 2 required positional arguments: 'a' and 'b'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_missing_heap_cleanup.py",
    "content": "# Test that heap values are properly cleaned up when missing required arg error occurs\ndef f(a, b, c):\n    return a\n\n\n# The list [1, 2, 3] should be cleaned up when the error occurs\n# because 'c' is missing\nf([1, 2, 3], [4, 5])\n# Raise=TypeError(\"f() missing 1 required positional argument: 'c'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_missing_kwonly.py",
    "content": "# Test: missing required keyword-only argument\ndef f(a, *, b):\n    return a + b\n\n\nf(1)\n# Raise=TypeError(\"f() missing 1 required keyword-only argument: 'b'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_missing_posonly_with_kwarg.py",
    "content": "# Test: missing positional-only when keyword is provided for other param\ndef f(a, /, b):\n    return a + b\n\n\nf(b=2)\n# Raise=TypeError(\"f() missing 1 required positional argument: 'a'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_missing_with_posonly.py",
    "content": "# Test: missing required argument with positional-only params\ndef f(a, b, /, c):\n    return a + b + c\n\n\nf(1, 2)\n# Raise=TypeError(\"f() missing 1 required positional argument: 'c'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_posonly_as_kwarg.py",
    "content": "# Test: positional-only parameter passed as keyword argument\ndef f(a, b, /, c):\n    return a + b + c\n\n\nf(1, b=2, c=3)\n# Raise=TypeError(\"f() got some positional-only arguments passed as keyword arguments: 'b'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_posonly_first_as_kwarg.py",
    "content": "# Test: first positional-only parameter passed as keyword argument\ndef f(a, /, b):\n    return a + b\n\n\nf(a=1, b=2)\n# Raise=TypeError(\"f() got some positional-only arguments passed as keyword arguments: 'a'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_too_many_posonly.py",
    "content": "# Test: too many positional arguments with positional-only params\ndef f(a, b, /):\n    return a + b\n\n\nf(1, 2, 3)\n# Raise=TypeError('f() takes 2 positional arguments but 3 were given')\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_too_many_with_kwonly.py",
    "content": "# Test: too many positional arguments with keyword-only params\ndef f(a, *, b):\n    return a + b\n\n\nf(1, 2, b=3)\n# Raise=TypeError('f() takes 1 positional argument but 2 positional arguments (and 1 keyword-only argument) were given')\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unexpected_kwarg.py",
    "content": "# Test: unexpected keyword argument\ndef f(a, b):\n    return a + b\n\n\nf(1, 2, c=3)\n# Raise=TypeError(\"f() got an unexpected keyword argument 'c'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unexpected_kwarg_cleanup.py",
    "content": "# Test that heap values are properly cleaned up when unexpected kwarg error occurs\ndef f(a, b):\n    return a\n\n\n# The list [1, 2, 3] should be cleaned up when the error occurs\n# because 'c' is an unexpected keyword argument\nf([1, 2, 3], [4, 5], c=[6, 7])\n# Raise=TypeError(\"f() got an unexpected keyword argument 'c'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unexpected_kwarg_quote.py",
    "content": "def f(a):\n    pass\n\n\nf(1, **{\"foo'\": 2})\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"function__err_unexpected_kwarg_quote.py\", line 5, in <module>\n    f(1, **{\"foo'\": 2})\n    ~~~~~~~~~~~~~~~~~~~\nTypeError: f() got an unexpected keyword argument 'foo''\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unexpected_kwarg_simple.py",
    "content": "# Test: unexpected keyword argument on single-param function\ndef f(a):\n    return a\n\n\nf(1, b=2)\n# Raise=TypeError(\"f() got an unexpected keyword argument 'b'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unpack_duplicate_arg.py",
    "content": "def f(a, b):\n    return a + b\n\n\nf(a=1, **{'a': 2})\n# Raise=TypeError(\"f() got multiple values for keyword argument 'a'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unpack_duplicate_heap.py",
    "content": "# Test that heap values are cleaned up when duplicate kwargs error occurs\ndef f(a, b):\n    return a\n\n\n# The list value in the dict should be cleaned up when the error occurs\nf(a=[1, 2, 3], **{'a': [4, 5, 6]})\n# Raise=TypeError(\"f() got multiple values for keyword argument 'a'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unpack_int.py",
    "content": "def f(a, b):\n    return a + b\n\n\nf(1, **42)\n# Raise=TypeError('f() argument after ** must be a mapping, not int')\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unpack_nonstring_key.py",
    "content": "def foo(a):\n    return a\n\n\nfoo(**{1: 'value'})\n# Raise=TypeError('keywords must be strings')\n"
  },
  {
    "path": "crates/monty/test_cases/function__err_unpack_not_mapping.py",
    "content": "def f(a, b):\n    return a + b\n\n\nf(1, **[2])\n# Raise=TypeError('f() argument after ** must be a mapping, not list')\n"
  },
  {
    "path": "crates/monty/test_cases/function__kwargs_unpacking.py",
    "content": "# === Basic **kwargs unpacking ===\ndef greet(name, greeting):\n    return f'{greeting}, {name}!'\n\n\nopts = {'greeting': 'Hi'}\nassert greet('Alice', **opts) == 'Hi, Alice!', 'basic **kwargs unpacking'\n\n# === Dict literal unpacking ===\nassert greet('Charlie', **{'greeting': 'Hey'}) == 'Hey, Charlie!', 'dict literal unpacking'\n\n\n# === Multiple kwargs in unpacked dict ===\ndef format_msg(msg, prefix, suffix):\n    return f'{prefix}{msg}{suffix}'\n\n\nassert format_msg('test', **{'prefix': '[', 'suffix': ']'}) == '[test]', 'multiple kwargs unpacking'\n\n# === Combining regular kwargs with **kwargs ===\nassert format_msg('hello', prefix='> ', **{'suffix': '!'}) == '> hello!', 'regular kwargs with **kwargs'\n\n\n# === **kwargs with positional args ===\ndef add_all(a, b, c):\n    return a + b + c\n\n\nassert add_all(1, 2, **{'c': 3}) == 6, '**kwargs with positional args'\nassert add_all(1, **{'b': 2, 'c': 3}) == 6, '**kwargs providing multiple args'\n\n# === Variable dict unpacking ===\nsettings = {'prefix': '>>> ', 'suffix': ' <<<'}\nassert format_msg('output', **settings) == '>>> output <<<', 'variable dict unpacking'\n\n\n# === Unpacking with keyword-only args ===\ndef kwonly_func(a, *, b, c):\n    return a + b + c\n\n\nassert kwonly_func(1, **{'b': 2, 'c': 3}) == 6, '**kwargs with keyword-only args'\n\n\n# === Empty dict unpacking with all args provided ===\ndef simple(x, y):\n    return x + y\n\n\nassert simple(1, 2, **{}) == 3, 'empty dict unpacking'\n\n\n# === All kwargs from unpacking ===\ndef all_kwargs(a, b, c):\n    return a * 100 + b * 10 + c\n\n\nassert all_kwargs(**{'a': 1, 'b': 2, 'c': 3}) == 123, 'all args from **kwargs'\nassert all_kwargs(**{'c': 7, 'a': 4, 'b': 5}) == 457, 'all args from **kwargs different order'\n\n\n# === Dynamic **kwargs keys ===\ndef kwonly_echo(*, keyword):\n    return keyword\n\n\nkey_name = 'k' + 'e' + 'y' + 'w' + 'o' + 'r' + 'd'\nassert kwonly_echo(**{key_name: 'dynamic'}) == 'dynamic', 'runtime string key matches kw-only param'\n\n\n# ============================================================\n# *args unpacking tests (function calls)\n# ============================================================\n\n\n# === *args with zero args ===\ndef no_args():\n    return 'ok'\n\n\nassert no_args(*[]) == 'ok', '*args with empty list'\nassert no_args(*()) == 'ok', '*args with empty tuple'\n\n\n# === *args with one arg ===\ndef one_arg(x):\n    return x * 2\n\n\nassert one_arg(*[5]) == 10, '*args with one item list'\nassert one_arg(*(7,)) == 14, '*args with one item tuple'\n\n\n# === *args with two args ===\ndef two_args(a, b):\n    return a + b\n\n\nassert two_args(*[1, 2]) == 3, '*args with two item list'\nassert two_args(*(3, 4)) == 7, '*args with two item tuple'\n\n\n# === *args with three+ args ===\ndef many_args(a, b, c, d):\n    return a + b + c + d\n\n\nassert many_args(*[1, 2, 3, 4]) == 10, '*args with four items'\nassert many_args(*(10, 20, 30, 40)) == 100, '*args with tuple four items'\n\n\n# === Mixed positional and *args ===\nassert two_args(1, *[2]) == 3, 'pos + *args'\nassert many_args(1, 2, *[3, 4]) == 10, 'two pos + *args'\n\n\n# === *args with heap-allocated values ===\ndef list_arg(lst):\n    return len(lst)\n\n\nmy_list = [1, 2, 3]\nassert list_arg(*[my_list]) == 3, '*args with list value'\n\n\n# ============================================================\n# Combined *args and **kwargs (function calls)\n# ============================================================\n\n\n# === *args and **kwargs together ===\ndef mixed_func(a, b, c):\n    return f'{a}-{b}-{c}'\n\n\nassert mixed_func(*[1], **{'b': 2, 'c': 3}) == '1-2-3', '*args and **kwargs'\nassert mixed_func(*[1, 2], **{'c': 3}) == '1-2-3', 'two *args and **kwargs'\n\n\n# === *args tuple with **kwargs ===\nargs_tuple = (10, 20)\nkwargs_dict = {'c': 30}\nassert many_args(*args_tuple, **kwargs_dict, d=40) == 100, '*args tuple + **kwargs + regular kwarg'\n\n\n# === Empty *args with **kwargs ===\nassert mixed_func(*[], **{'a': 'x', 'b': 'y', 'c': 'z'}) == 'x-y-z', 'empty *args with **kwargs'\n\n\n# === *args with empty **kwargs ===\nassert two_args(*[5, 6], **{}) == 11, '*args with empty **kwargs'\n\n\n# === All combinations: pos, *args, kwargs, **kwargs ===\ndef full_func(a, b, c, d):\n    return a * 1000 + b * 100 + c * 10 + d\n\n\nassert full_func(1, *[2], c=3, **{'d': 4}) == 1234, 'pos + *args + kwarg + **kwargs'\n\n\n# === *args with heap values and **kwargs ===\ndef heap_func(lst, dct):\n    return len(lst) + len(dct)\n\n\nlist_val = [1, 2, 3]\ndict_val = {'a': 1}\nassert heap_func(*[list_val], **{'dct': dict_val}) == 4, '*args and **kwargs with heap values'\n\n\n# === Both *args and **kwargs empty ===\nassert no_args(*[], **{}) == 'ok', 'empty *args and empty **kwargs'\n"
  },
  {
    "path": "crates/monty/test_cases/function__ops.py",
    "content": "# === Basic function calls ===\ndef f_no_args():\n    return 1\n\n\nassert f_no_args() == 1, 'no args'\n\n\ndef f_one_arg(x):\n    return x\n\n\nassert f_one_arg(42) == 42, 'one arg'\n\n\ndef add(a, b):\n    return a + b\n\n\nassert add(1, 2) == 3, 'two args'\n\n\ndef sum3(a, b, c):\n    return a + b + c\n\n\nassert sum3(1, 2, 3) == 6, 'three args'\n\n\n# === Local variables ===\ndef f_local():\n    x = 42\n    return x\n\n\nassert f_local() == 42, 'local var'\n\n\ndef f_local_from_arg(x):\n    y = x + 1\n    return y\n\n\nassert f_local_from_arg(10) == 11, 'local var from arg'\n\n\ndef f_local_list():\n    items = [1, 2, 3]\n    return items\n\n\nassert f_local_list() == [1, 2, 3], 'local var list'\n\n\ndef f_local_modify_list():\n    items = [1, 2]\n    items.append(3)\n    return items\n\n\nassert f_local_modify_list() == [1, 2, 3], 'local var modify list'\n\n\ndef f_local_multiple():\n    a = 1\n    b = 2\n    c = 3\n    return a + b + c\n\n\nassert f_local_multiple() == 6, 'local var multiple'\n\n\ndef f_local_reassign():\n    x = 1\n    x = 2\n    x = 3\n    return x\n\n\nassert f_local_reassign() == 3, 'local var reassign'\n\n\n# === Nested functions ===\ndef nested_basic():\n    def bar():\n        return 1\n\n    return bar() + 1\n\n\nassert nested_basic() == 2, 'nested basic'\n\n\ndef nested_deep():\n    def level2():\n        def level3():\n            return 42\n\n        return level3()\n\n    return level2()\n\n\nassert nested_deep() == 42, 'nested deep'\n\n\ndef nested_multiple_calls():\n    def inner():\n        return 10\n\n    return inner() + inner() + inner()\n\n\nassert nested_multiple_calls() == 30, 'nested multiple calls'\n\n\ndef nested_two_inner():\n    def add():\n        return 1\n\n    def sub():\n        return 2\n\n    return add() + sub()\n\n\nassert nested_two_inner() == 3, 'nested two inner'\n\n\ndef nested_with_args(x):\n    def inner(y):\n        return y + y\n\n    return inner(x) + 1\n\n\nassert nested_with_args(5) == 11, 'nested with args'\n\n\n# === Function equality ===\ndef eq_test():\n    return 1\n\n\ndef eq_test2():\n    return 1\n\n\n# Same function is equal to itself\nassert eq_test == eq_test, 'function equals itself'\nassert not (eq_test != eq_test), 'function not-not-equals itself'\n\n# Different functions are not equal (even with same body)\nassert not (eq_test == eq_test2), 'different functions not equal'\nassert eq_test != eq_test2, 'different functions are not equal'\n\n# Function assigned to variable is still equal\nf_alias = eq_test\nassert f_alias == eq_test, 'function alias equals original'\nassert eq_test == f_alias, 'original equals function alias'\n\n\n# === Builtin equality ===\n# Same builtin is equal to itself\nassert len == len, 'builtin equals itself'\nassert print == print, 'print equals itself'\nassert not (len != len), 'builtin not-not-equals itself'\n\n# Builtin identity (is)\nassert print is print, 'print is print'\nassert len is len, 'len is len'\nassert not (len is print), 'len is not print'\n\n# Different builtins are not equal\nassert not (len == print), 'different builtins not equal'\nassert len != print, 'different builtins are not equal'\n\n# Builtin assigned to variable is still equal\nlen_alias = len\nassert len_alias == len, 'builtin alias equals original'\nassert len_alias is len, 'builtin alias is original'\n\n\n# === Exception type equality ===\n# Note: Using == instead of 'is' to explicitly test the __eq__ implementation\nassert ValueError == ValueError, 'exc type equals itself'\nassert TypeError == TypeError, 'exc type equals itself 2'\nassert not (ValueError != ValueError), 'exc type not-not-equals itself'\n\nassert not (ValueError == TypeError), 'different exc types not equal'\nassert ValueError != TypeError, 'different exc types are not equal'\n\nexc_alias = ValueError\nassert exc_alias == ValueError, 'exc type alias equals original'\n\n\n# === Closure equality ===\ndef make_adder(n):\n    def adder(x):\n        return x + n\n\n    return adder\n\n\nadd1 = make_adder(1)\nadd2 = make_adder(2)\nadd1_again = make_adder(1)\n\n# Same closure instance equals itself\nassert add1 == add1, 'closure equals itself'\nassert not (add1 != add1), 'closure not-not-equals itself'\n\n# Different closure instances are not equal (even with same captured value)\nassert not (add1 == add1_again), 'different closure instances not equal'\nassert add1 != add1_again, 'different closure instances are not equal'\n\n# Different closure instances with different captured values\nassert not (add1 == add2), 'closures with diff captured values not equal'\nassert add1 != add2, 'closures with diff captured values are not equal'\n\n\n# === Cross-type inequality ===\ndef cross_test():\n    return 1\n\n\nassert not (cross_test == len), 'function not equal to builtin'\nassert not (len == cross_test), 'builtin not equal to function'\nassert not (cross_test == ValueError), 'function not equal to exc type'\nassert not (ValueError == cross_test), 'exc type not equal to function'\nassert not (len == ValueError), 'builtin not equal to exc type'\nassert not (ValueError == len), 'exc type not equal to builtin'\n\n# Callables not equal to other types\nassert not (len == 1), 'builtin not equal to int'\nassert not (len == 'len'), 'builtin not equal to string'\nassert not (cross_test == None), 'function not equal to None'\nassert not (ValueError == None), 'exc type not equal to None'\n\n\n# === Parameter shadowing global variables ===\n# Function parameters should shadow global variables with the same name\nx = 5\n\n\ndef shadow_single(x):\n    return x + 1\n\n\n# When called with 10, param x=10 should be used, not global x=5\nassert shadow_single(10) == 11, 'param shadows global - single param'\n\ny = 3\n\n\ndef shadow_multiple(x, y):\n    return x + y\n\n\n# When called with (20, 30), params should be used, not globals x=5, y=3\nassert shadow_multiple(20, 30) == 50, 'param shadows global - multiple params'\n\n\ndef shadow_uses_global_too(x):\n    # x is param, y is global\n    return x + y\n\n\n# x=100 (param), y=3 (global), so 100 + 3 = 103\nassert shadow_uses_global_too(100) == 103, 'param shadows but can still access other globals'\n\n\ndef shadow_with_default(x=99):\n    return x + 1\n\n\n# When called with argument, param shadows global\nassert shadow_with_default(10) == 11, 'param with default shadows global'\n# When called without argument, default is used (not global)\nassert shadow_with_default() == 100, 'param default used, not global'\n\n\n# Global is still accessible outside the function\nassert x == 5, 'global still accessible after function that shadows it'\nassert y == 3, 'other global still accessible'\n\n\n# Verify global can still be used as argument\ndef double(x):\n    return x * 2\n\n\nassert double(x) == 10, 'global used as argument, param shadows inside'\n"
  },
  {
    "path": "crates/monty/test_cases/function__return_none.py",
    "content": "# === Bare return statement ===\n# Test functions with bare return (no value)\n\n\ndef early_exit():\n    return\n\n\nassert early_exit() is None, 'bare return returns None'\n\n\ndef conditional_early_exit(x):\n    if x < 0:\n        return\n    return x * 2\n\n\nassert conditional_early_exit(-5) is None, 'conditional early return'\nassert conditional_early_exit(5) == 10, 'conditional normal return'\n\n\ndef multiple_bare_returns(x):\n    if x == 0:\n        return\n    if x == 1:\n        return\n    return x\n\n\nassert multiple_bare_returns(0) is None, 'first bare return'\nassert multiple_bare_returns(1) is None, 'second bare return'\nassert multiple_bare_returns(2) == 2, 'fall through to value return'\n\n\ndef nested_bare_return():\n    def inner():\n        return\n\n    return inner()\n\n\nassert nested_bare_return() is None, 'nested bare return'\n"
  },
  {
    "path": "crates/monty/test_cases/function__signatures.py",
    "content": "# === Basic functions ===\ndef simple(a, b, c):\n    return a + b + c\n\n\nassert simple(1, 2, 3) == 6, 'simple function'\nassert simple(10, 20, 30) == 60, 'simple function with larger values'\n\n\n# === Positional-only parameters ===\ndef pos_only(a, b, /, c):\n    return a + b + c\n\n\nassert pos_only(1, 2, 3) == 6, 'positional-only params'\nassert pos_only(5, 5, 5) == 15, 'positional-only all same'\nassert pos_only(5, 5, c=5) == 15, 'positional-only all same'\n\n\n# === All positional-only ===\ndef all_pos_only(a, b, c, /):\n    return a + b + c\n\n\nassert all_pos_only(1, 2, 3) == 6, 'all positional-only'\n\n\n# === Multiple parameter groups ===\ndef multi_group(a, /, b, c):\n    return f'a={a} b={b} c={c}'\n\n\nassert multi_group(1, 2, 3) == 'a=1 b=2 c=3', 'mixed positional-only and regular'\nassert multi_group(1, b=2, c=3) == 'a=1 b=2 c=3', 'mixed positional-only and regular'\nassert multi_group(1, c=3, b=2) == 'a=1 b=2 c=3', 'mixed positional-only and regular'\n\n\n# === Call-site *args unpacking ===\ndef collect_all(*values):\n    return values\n\n\nsource_tuple = (1, 2, 3)\nassert collect_all(*source_tuple) == (1, 2, 3), 'tuple unpacked with *args'\n\nsource_list = [4, 5]\nassert collect_all(0, *source_list) == (0, 4, 5), 'positional args followed by *args'\n"
  },
  {
    "path": "crates/monty/test_cases/function__too_few_args_all.py",
    "content": "def f(a, b, c):\n    return a + b + c\n\n\nf()\n# Raise=TypeError(\"f() missing 3 required positional arguments: 'a', 'b', and 'c'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__too_few_args_one.py",
    "content": "def f(x):\n    return x\n\n\nf()\n# Raise=TypeError(\"f() missing 1 required positional argument: 'x'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__too_few_args_two.py",
    "content": "def f(a, b):\n    return a + b\n\n\nf(1)\n# Raise=TypeError(\"f() missing 1 required positional argument: 'b'\")\n"
  },
  {
    "path": "crates/monty/test_cases/function__too_many_args_one.py",
    "content": "def f(x):\n    return x\n\n\nf(1, 2)\n# Raise=TypeError('f() takes 1 positional argument but 2 were given')\n"
  },
  {
    "path": "crates/monty/test_cases/function__too_many_args_two.py",
    "content": "def f(a, b):\n    return a + b\n\n\nf(1, 2, 3)\n# Raise=TypeError('f() takes 2 positional arguments but 3 were given')\n"
  },
  {
    "path": "crates/monty/test_cases/function__too_many_args_zero.py",
    "content": "def f():\n    return 1\n\n\nf(42)\n# Raise=TypeError('f() takes 0 positional arguments but 1 was given')\n"
  },
  {
    "path": "crates/monty/test_cases/global__error_assigned_before.py",
    "content": "def f():\n    x = 1\n    global x  # type: ignore[reportAssignmentBeforeGlobalDeclaration]\n\n\nf()\n# Raise=SyntaxError(\"name 'x' is assigned to before global declaration\")\n"
  },
  {
    "path": "crates/monty/test_cases/global__ops.py",
    "content": "# === Basic global read/write ===\nx1 = 42\n\n\ndef read_explicit():\n    global x1\n    return x1\n\n\nassert read_explicit() == 42, 'explicit global read'\n\n\nx2 = 1\n\n\ndef write_explicit():\n    global x2\n    x2 = 2\n\n\nwrite_explicit()\nassert x2 == 2, 'explicit global write'\n\n\nx3 = 42\n\n\ndef read_implicit():\n    return x3  # no local x3, reads global\n\n\nassert read_implicit() == 42, 'implicit global read'\n\n\n# === Multiple functions sharing global ===\ncounter1 = 0\n\n\ndef inc():\n    global counter1\n    counter1 = counter1 + 1\n\n\ndef get_counter():\n    return counter1\n\n\ninc()\ninc()\nassert get_counter() == 2, 'multiple functions sharing global'\n\n\n# === Mutating global containers (no 'global' needed) ===\ndata1 = {'a': 1}\n\n\ndef add_dict_entry():\n    data1['b'] = 2\n\n\nadd_dict_entry()\nassert data1 == {'a': 1, 'b': 2}, 'mutate global dict'\n\n\nitems1 = [1, 2]\n\n\ndef append_list_item():\n    items1.append(3)\n\n\nappend_list_item()\nassert items1 == [1, 2, 3], 'mutate global list append'\n\n\nitems2 = ['a', 'c']\n\n\ndef insert_list_item():\n    items2.insert(1, 'b')\n\n\ninsert_list_item()\nassert items2 == ['a', 'b', 'c'], 'mutate global list insert'\n\n\nitems3 = []\n\n\ndef build_list():\n    items3.append(1)\n    items3.append(2)\n    items3.append(3)\n\n\nbuild_list()\nassert items3 == [1, 2, 3], 'mutate global list multiple'\n\n\n# === Reassigning global containers (requires 'global') ===\nitems4 = [1, 2]\n\n\ndef replace_list():\n    global items4\n    items4 = [3, 4, 5]\n\n\nreplace_list()\nassert items4 == [3, 4, 5], 'reassign global list'\n\n\n# === Nested functions with global ===\nx4 = 1\n\n\ndef outer_global():\n    def inner():\n        global x4\n        x4 = 10\n\n    inner()\n\n\nouter_global()\nassert x4 == 10, 'nested inner global write'\n\n\nx5 = 42\n\n\ndef outer_read():\n    def inner():\n        return x5  # reads global\n\n    return inner()\n\n\nassert outer_read() == 42, 'nested inner global read'\n\n\n# === Shadowing ===\nx6 = 10\n\n\ndef shadow_local():\n    x6 = 20  # creates local (shadows global)\n    return x6\n\n\nassert shadow_local() == 20, 'local shadows global'\n\n\nx7 = 10\n\n\ndef shadow_unchanged():\n    x7 = 99  # local\n    return x7\n\n\nassert shadow_unchanged() == 99, 'shadowing returns local'\nassert x7 == 10, 'global unchanged after shadowing'\n"
  },
  {
    "path": "crates/monty/test_cases/hash__dict_unhashable.py",
    "content": "hash({})\n# Raise=TypeError(\"unhashable type: 'dict'\")\n"
  },
  {
    "path": "crates/monty/test_cases/hash__list_unhashable.py",
    "content": "hash([1, 2, 3])\n# Raise=TypeError(\"unhashable type: 'list'\")\n"
  },
  {
    "path": "crates/monty/test_cases/hash__ops.py",
    "content": "# === Hash returns int type ===\nassert isinstance(hash(42), int), 'hash returns int type'\nassert isinstance(hash('hello'), int), 'hash of str returns int'\nassert isinstance(hash((1, 2, 3)), int), 'hash of tuple returns int'\nassert isinstance(hash(3.14), int), 'hash of float returns int'\n\n# === Hash consistency for same values ===\nassert hash(42) == hash(42), 'int hash consistent'\nassert hash(-1) == hash(-1), 'negative int hash consistent'\nassert hash(0) == hash(0), 'zero hash consistent'\nassert hash('hello') == hash('hello'), 'str hash consistent'\nassert hash('') == hash(''), 'empty str hash consistent'\nassert hash(b'hello') == hash(b'hello'), 'bytes hash consistent'\nassert hash(b'') == hash(b''), 'empty bytes hash consistent'\nassert hash(None) == hash(None), 'None hash consistent'\nassert hash(True) == hash(True), 'True hash consistent'\nassert hash(False) == hash(False), 'False hash consistent'\nassert hash((1, 2, 3)) == hash((1, 2, 3)), 'tuple hash consistent'\nassert hash(()) == hash(()), 'empty tuple hash consistent'\nassert hash((1,)) == hash((1,)), 'single element tuple hash consistent'\nassert hash(3.14) == hash(3.14), 'float hash consistent'\nassert hash(0.0) == hash(0.0), 'zero float hash consistent'\nassert hash(-0.0) == hash(-0.0), 'negative zero float hash consistent'\nassert hash(...) == hash(...), 'ellipsis hash consistent'\n\n# === Range hash consistency ===\nassert hash(range(10)) == hash(range(10)), 'range hash consistent'\nassert hash(range(0)) == hash(range(0)), 'empty range hash consistent'\nassert hash(range(1, 10)) == hash(range(1, 10)), 'range with start hash consistent'\nassert hash(range(1, 10, 2)) == hash(range(1, 10, 2)), 'range with step hash consistent'\nassert hash(range(-5, 5)) == hash(range(-5, 5)), 'negative start range hash consistent'\n\n# === Different range values should hash differently ===\nassert hash(range(10)) != hash(range(11)), 'different range stop hashes differently'\nassert hash(range(10)) != hash(range(1, 10)), 'range with different start hashes differently'\nassert hash(range(10)) != hash(range(0, 10, 2)), 'range with step hashes differently'\nassert hash(range(1, 10, 2)) != hash(range(1, 10, 3)), 'different steps hash differently'\n\n# === Different values should hash differently ===\nassert hash(1) != hash(2), 'different ints hash differently'\nassert hash('a') != hash('b'), 'different strs hash differently'\nassert hash(b'a') != hash(b'b'), 'different bytes hash differently'\nassert hash((1, 2)) != hash((1, 3)), 'different tuples hash differently'\nassert hash((1, 2)) != hash((2, 1)), 'tuple order matters for hash'\nassert hash(True) != hash(False), 'True and False hash differently'\nassert hash(3.14) != hash(2.71), 'different floats hash differently'\n\n# === Type differentiation for clearly different types ===\nassert hash(()) != hash(''), 'empty tuple and empty str hash differently'\nassert hash('1') != hash(1), 'str \"1\" and int 1 hash differently'\nassert hash(b'1') != hash(1), 'bytes b\"1\" and int 1 hash differently'\n\n# === Nested tuple hashing ===\nassert hash((1, (2, 3))) == hash((1, (2, 3))), 'nested tuple hash consistent'\nassert hash((1, (2, 3))) != hash((1, (2, 4))), 'nested tuples with different inner values hash differently'\nassert hash(((1, 2), (3, 4))) == hash(((1, 2), (3, 4))), 'tuple of tuples hash consistent'\n\n# === String/bytes content equality across representations ===\n# Interned strings and heap strings with same content should hash the same\ns1 = 'test'\ns2 = 'te' + 'st'\nassert hash(s1) == hash(s2), 'concatenated string hashes same as literal'\n\nb1 = b'test'\nb2 = b'te' + b'st'\nassert hash(b1) == hash(b2), 'concatenated bytes hashes same as literal'\n\n\n# === Function hashing ===\ndef f():\n    pass\n\n\ndef g():\n    pass\n\n\nassert hash(f) == hash(f), 'function hash consistent'\nassert hash(g) == hash(g), 'different function hash consistent'\nassert hash(f) != hash(g), 'different functions hash differently'\n\n# === Builtin function hashing ===\nassert hash(len) == hash(len), 'builtin hash consistent'\nassert hash(print) == hash(print), 'print builtin hash consistent'\nassert hash(len) != hash(print), 'different builtins hash differently'\n\n# === Builtin type hashing ===\nassert hash(int) == hash(int), 'int type hash consistent'\nassert hash(str) == hash(str), 'str type hash consistent'\nassert hash(int) != hash(str), 'different types hash differently'\nassert hash(int) != hash(float), 'int and float types hash differently'\n\n# === Exception type hashing ===\nassert hash(ValueError) == hash(ValueError), 'exception type hash consistent'\nassert hash(TypeError) == hash(TypeError), 'TypeError hash consistent'\nassert hash(ValueError) != hash(TypeError), 'different exception types hash differently'\n\n# === Dict key behavior with hashes ===\n# Verify that hash consistency works with dict lookups\nd = {}\nd[42] = 'int'\nd['hello'] = 'str'\nd[(1, 2)] = 'tuple'\nd[range(5)] = 'range'\nd[3.14] = 'float'\nd[None] = 'none'\n\nassert d[42] == 'int', 'int dict key works'\nassert d['hello'] == 'str', 'str dict key works'\nassert d[(1, 2)] == 'tuple', 'tuple dict key works'\nassert d[range(5)] == 'range', 'range dict key works'\nassert d[3.14] == 'float', 'float dict key works'\nassert d[None] == 'none', 'None dict key works'\n\n# === Multiple ranges as dict keys ===\nrd = {}\nrd[range(5)] = 'a'\nrd[range(10)] = 'b'\nrd[range(1, 5)] = 'c'\nrd[range(0, 5, 2)] = 'd'\n\nassert rd[range(5)] == 'a', 'range(5) key retrieval'\nassert rd[range(10)] == 'b', 'range(10) key retrieval'\nassert rd[range(1, 5)] == 'c', 'range(1,5) key retrieval'\nassert rd[range(0, 5, 2)] == 'd', 'range with step key retrieval'\nassert len(rd) == 4, 'all ranges stored as distinct keys'\n\n\n# === Functions as dict keys ===\ndef key_fn():\n    pass\n\n\nfd = {}\nfd[key_fn] = 'func_value'\nassert fd[key_fn] == 'func_value', 'function as dict key works'\n\n# === Builtins as dict keys ===\nbd = {}\nbd[len] = 'len_value'\nbd[print] = 'print_value'\nassert bd[len] == 'len_value', 'builtin len as dict key'\nassert bd[print] == 'print_value', 'builtin print as dict key'\nassert len(bd) == 2, 'different builtins are distinct keys'\n\n# === Types as dict keys ===\ntd = {}\ntd[int] = 'int_type'\ntd[str] = 'str_type'\ntd[ValueError] = 'value_error'\nassert td[int] == 'int_type', 'int type as dict key'\nassert td[str] == 'str_type', 'str type as dict key'\nassert td[ValueError] == 'value_error', 'exception type as dict key'\n"
  },
  {
    "path": "crates/monty/test_cases/id__bytes_literals_distinct.py",
    "content": "# xfail=cpython\nid(b'test') == id(b'test')\n# Return=False\n"
  },
  {
    "path": "crates/monty/test_cases/id__int_copy_distinct.py",
    "content": "# value-based identity: same value = same id\nx = 100\ny = x\nid(x) == id(y)\n# Return=True\n"
  },
  {
    "path": "crates/monty/test_cases/id__is_number_is_number.py",
    "content": "# value-based identity: same value = same identity\n1 is 1\n# Return=True\n"
  },
  {
    "path": "crates/monty/test_cases/id__non_overlapping_lifetimes_distinct_types.py",
    "content": "# xfail=cpython\n# === Heap types may have the same id if lifetimes do not overlap ===\n# See https://docs.python.org/3/library/functions.html#id\n# for Cpython it happens to be the case that for different types they end\n# up being allocated in different memory locations, but this is not guaranteed by the language spec\nassert id([]) == id([]), 'empty list may have same id'\nassert id([]) == id({}), 'empty list may have same id as empty dict'\nassert id([]) == id((1,)), 'empty list may have same id as tuple'\nassert id((1, 2)) == id((1, 2)), 'non-empty tuple may have same id'\nassert id([1, 2]) == id([1, 2]), 'non-empty list may have same id'\n"
  },
  {
    "path": "crates/monty/test_cases/id__non_overlapping_lifetimes_same_types.py",
    "content": "# === Heap types may have the same id if lifetimes do not overlap ===\n# See https://docs.python.org/3/library/functions.html#id\nassert id([]) == id([]), 'empty list may have same id'\nassert id({}) == id({}), 'empty dict may have same id'\nassert id((1, 2)) == id((1, 2)), 'non-empty tuple may have same id'\nassert id([1, 2]) == id([1, 2]), 'non-empty list may have same id'\n"
  },
  {
    "path": "crates/monty/test_cases/id__ops.py",
    "content": "# === id() returns int type ===\nassert isinstance(id(None), int), 'id returns int type'\nassert isinstance(id([]), int), 'id of list returns int'\nassert isinstance(id('hello'), int), 'id of str returns int'\nassert isinstance(id(42), int), 'id of int returns int'\n\n# === Identity operator (is) ===\nassert (True is True) == True, 'is True'\nassert (False is False) == True, 'is False'\nassert (None is None) == True, 'is None'\nassert (... is ...) == True, 'is Ellipsis'\n\n# === Identity operator (is not) ===\nassert (True is not True) == False, 'is not True'\nassert (True is not False) == True, 'is not False'\n\n# === Singleton identity ===\nassert id(None) == id(None), 'None singleton'\nassert id(True) == id(True), 'True singleton'\nassert id(False) == id(False), 'False singleton'\nassert id(...) == id(...), 'Ellipsis singleton'\n\n# bool and int are distinct\nassert id(True) != id(1), 'True is not 1'\nassert id(False) != id(0), 'False is not 0'\n\n# distinct singletons\nassert id(None) != id(True), 'None is not True'\nassert id(None) != id(False), 'None is not False'\nassert id(None) != id(...), 'None is not Ellipsis'\n\n# === Integer identity ===\nassert id(10) != id(20), 'different ints distinct'\n\n# === Float identity ===\nassert id(1.0) != id(2.0), 'different floats distinct'\n\n# === List assignment shares identity ===\nlst = [1, 2]\nref = lst\nassert id(lst) == id(ref), 'list assignment shared'\nassert lst is ref, 'list is same'\n\n# === Variable identity is stable ===\nlst = [1, 2]\nassert id(lst) == id(lst), 'var id stable'\n\n# === List mutation preserves identity ===\na = [1, 2]\nb = a\nb.append(3)\nassert a is b, 'list mutate preserves identity'\n\n# === Mixed types have distinct ids ===\nassert id(1) != id('1'), 'int vs str distinct'\n\n# === Tuple singleton is guaranteed to have a unique id ===\nassert id([]) != id(()), 'list vs tuple singleton distinct'\nassert id({}) != id(()), 'dict vs tuple singleton distinct'\nassert id(1) != id(()), 'int vs tuple singleton distinct'\n\n# === Multiple refs share id ===\nx = [1, 2]\ny = x\nz = y\nassert id(x) == id(y), 'multiple refs share id xy'\nassert id(y) == id(z), 'multiple refs share id yz'\n\n# === String assignment shares identity ===\ns = 'hello'\nr = s\nassert id(s) == id(r), 'str assignment shared'\n\n# === Bytes assignment shares identity ===\nb = b'hello'\nr = b\nassert id(b) == id(r), 'bytes assignment shared'\n\n# === Tuple assignment shares identity ===\nt = (1, 2)\nr = t\nassert id(t) == id(r), 'tuple assignment shared'\n\n# === Boolean is tests ===\nassert (True is True) == True, 'bool is test'\nassert (False is False) == True, 'bool is test 2'\n\n# === Array is test ===\na = [1, 2]\nb = a\nassert (a is b) == True, 'array is test'\nassert (a is [1, 2]) == False, 'array is new literal'\n\n# === None is tests ===\nx = None\nassert (x is None) == True, 'var is None'\nassert (1 is None) == False, 'int is not None'\n"
  },
  {
    "path": "crates/monty/test_cases/id__str_literals_same.py",
    "content": "# With string interning, identical literals have the same id\nid('hello') == id('hello')\n# Return=True\n"
  },
  {
    "path": "crates/monty/test_cases/if__elif_else.py",
    "content": "# === Basic elif chains ===\n\n# if branch taken\nx = 0\nif True:\n    x = 1\nelif True:\n    x = 2\nassert x == 1, 'if branch should be taken when condition is True'\n\n# elif branch taken (first elif)\ny = 0\nif False:\n    y = 1\nelif True:\n    y = 2\nelif True:\n    y = 3\nassert y == 2, 'first elif should be taken when if is False and elif is True'\n\n# second elif taken\nz = 0\nif False:\n    z = 1\nelif False:\n    z = 2\nelif True:\n    z = 3\nassert z == 3, 'second elif should be taken'\n\n# else branch taken after if\na = 0\nif False:\n    a = 1\nelse:\n    a = 2\nassert a == 2, 'else should be taken when if is False'\n\n# else branch taken after elif chain\nb = 0\nif False:\n    b = 1\nelif False:\n    b = 2\nelif False:\n    b = 3\nelse:\n    b = 4\nassert b == 4, 'else should be taken when all conditions are False'\n\n# === Value-based conditions ===\n\nval = 5\n\nc = 0\nif val < 3:\n    c = 1\nelif val < 6:\n    c = 2\nelif val < 9:\n    c = 3\nelse:\n    c = 4\nassert c == 2, 'elif condition val < 6 should match for val=5'\n\nval2 = 10\nd = 0\nif val2 < 3:\n    d = 1\nelif val2 < 6:\n    d = 2\nelif val2 < 9:\n    d = 3\nelse:\n    d = 4\nassert d == 4, 'else should be taken for val2=10'\n\n# === Multiple statements in branches ===\n\ne = 0\nf = 0\nif False:\n    e = 1\n    f = 1\nelif True:\n    e = 2\n    f = 2\nelse:\n    e = 3\n    f = 3\nassert e == 2, 'first statement in elif executed'\nassert f == 2, 'second statement in elif executed'\n\n# === Nested if inside elif ===\n\ng = 0\nif False:\n    g = 1\nelif True:\n    if True:\n        g = 100\n    else:\n        g = 200\nelse:\n    g = 3\nassert g == 100, 'nested if inside elif should work'\n\n# nested if in else\nh = 0\nif False:\n    h = 1\nelif False:\n    h = 2\nelse:\n    if True:\n        h = 300\n    else:\n        h = 400\nassert h == 300, 'nested if inside else should work'\n\n# === Short-circuit evaluation ===\n\n# elif condition not evaluated if earlier branch taken\ncalled = False\n\n\ndef set_called():\n    global called\n    called = True\n    return True\n\n\ni = 0\nif True:\n    i = 1\nelif set_called():\n    i = 2\nassert i == 1, 'if branch taken'\nassert called == False, 'elif condition should not be evaluated when if branch is taken'\n\n# reset and test elif evaluation\ncalled = False\nj = 0\nif False:\n    j = 1\nelif set_called():\n    j = 2\nassert j == 2, 'elif branch taken'\nassert called == True, 'elif condition should be evaluated when if condition is False'\n\n# === Empty body handling (pass) ===\n\nk = 0\nif False:\n    pass\nelif True:\n    k = 1\nelse:\n    pass\nassert k == 1, 'elif body executes after if with pass'\n\n# === Boolean expression conditions ===\n\nand_result = 0\nif False and True:\n    and_result = 1\nelif True and True:\n    and_result = 2\nelse:\n    and_result = 3\nassert and_result == 2, 'elif with and condition'\n\nor_result = 0\nif False or False:\n    or_result = 1\nelif False or True:\n    or_result = 2\nelse:\n    or_result = 3\nassert or_result == 2, 'elif with or condition'\n\n# === Multiple conditions with and ===\n\nn = 5\no = 0\nif n > 1 and n < 3:\n    o = 1\nelif n > 3 and n < 7:\n    o = 2\nelse:\n    o = 3\nassert o == 2, 'elif with multiple and conditions'\n\n# === Variable assignment in conditions (walrus operator style via temp var) ===\n\n# Test value propagation through elif chain\np = 0\ntemp = 10\nif temp > 20:\n    p = 1\nelif temp > 5:\n    p = 2\nelif temp > 0:\n    p = 3\nelse:\n    p = 4\nassert p == 2, 'second condition matches temp=10'\n"
  },
  {
    "path": "crates/monty/test_cases/if__raise_elif.py",
    "content": "if False:\n    pass\nelif True:\n    raise ValueError('in elif body')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"if__raise_elif.py\", line 4, in <module>\n    raise ValueError('in elif body')\nValueError: in elif body\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/if__raise_else.py",
    "content": "if False:\n    pass\nelif False:\n    pass\nelse:\n    raise ValueError('in else body')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"if__raise_else.py\", line 6, in <module>\n    raise ValueError('in else body')\nValueError: in else body\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/if__raise_if.py",
    "content": "if True:\n    raise ValueError('in if body')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"if__raise_if.py\", line 2, in <module>\n    raise ValueError('in if body')\nValueError: in if body\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/if__raise_in_elif_condition.py",
    "content": "def fail():\n    raise ValueError('elif condition failed')\n\n\nif False:\n    x = 1\nelif fail():\n    x = 2\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"if__raise_in_elif_condition.py\", line 7, in <module>\n    elif fail():\n         ~~~~~~\n  File \"if__raise_in_elif_condition.py\", line 2, in fail\n    raise ValueError('elif condition failed')\nValueError: elif condition failed\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/if__raise_in_if_condition.py",
    "content": "def fail():\n    raise ValueError('condition failed')\n\n\nif fail():\n    x = 1\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"if__raise_in_if_condition.py\", line 5, in <module>\n    if fail():\n       ~~~~~~\n  File \"if__raise_in_if_condition.py\", line 2, in fail\n    raise ValueError('condition failed')\nValueError: condition failed\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/if_else_expr__all.py",
    "content": "# === Basic if/else ===\nassert (1 if True else 2) == 1, 'true condition'\nassert (1 if False else 2) == 2, 'false condition'\n\n# === Truthy/falsy values ===\nassert ('yes' if 1 else 'no') == 'yes', 'truthy int'\nassert ('yes' if 0 else 'no') == 'no', 'falsy int'\nassert ('yes' if 'a' else 'no') == 'yes', 'truthy str'\nassert ('yes' if '' else 'no') == 'no', 'falsy str'\nassert ('yes' if [1] else 'no') == 'yes', 'truthy list'\nassert ('yes' if [] else 'no') == 'no', 'falsy list'\nassert ('yes' if None else 'no') == 'no', 'None is falsy'\n\n# === Variables and comparisons ===\nx = 5\nassert (x if x > 0 else -x) == 5, 'positive x'\nx = -3\nassert (x if x > 0 else -x) == 3, 'negative x - abs'\n\n# === Nested if/else ===\na = 1\nb = 2\nc = 3\nassert ((a if a > b else b) if True else c) == 2, 'nested - outer true'\nassert ((a if a > b else b) if False else c) == 3, 'nested - outer false'\nassert (a if True else (b if True else c)) == 1, 'nested in else - not evaluated'\n\n# === Complex expressions ===\nassert (1 + 2 if True else 3 + 4) == 3, 'arithmetic in body'\nassert (1 + 2 if False else 3 + 4) == 7, 'arithmetic in orelse'\n\n# === With heap values (strings, lists) ===\ns1 = 'hello'\ns2 = 'world'\nassert (s1 if True else s2) == 'hello', 'string true branch'\nassert (s1 if False else s2) == 'world', 'string false branch'\n\nl1 = [1, 2]\nl2 = [3, 4]\nresult = l1 if True else l2\nassert result == [1, 2], 'list true branch'\nresult = l1 if False else l2\nassert result == [3, 4], 'list false branch'\n\n# === In f-strings ===\nval = 10\nassert f'{val if val > 5 else 0}' == '10', 'fstring with true branch'\nval = 3\nassert f'{val if val > 5 else 0}' == '0', 'fstring with false branch'\nassert f'value: {1 if True else 2}' == 'value: 1', 'fstring with prefix'\nassert f'{\"yes\" if 1 else \"no\"}' == 'yes', 'fstring with string result'\n\n# === F-string with format spec ===\nx = 42\nassert f'{x if True else 0:05d}' == '00042', 'fstring format spec with if/else'\n"
  },
  {
    "path": "crates/monty/test_cases/import__error_cannot_import.py",
    "content": "from sys import nonexistent\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"import__error_cannot_import.py\", line 1, in <module>\n    from sys import nonexistent\nImportError: cannot import name 'nonexistent' from 'sys' (unknown location)\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/import__error_module_not_found.py",
    "content": "import nonexistent_module\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"import__error_module_not_found.py\", line 1, in <module>\n    import nonexistent_module\nModuleNotFoundError: No module named 'nonexistent_module'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/import__local_scope.py",
    "content": "# Tests that import inside functions binds to local scope, not global\n\n# === Import statement inside function ===\ndef test_import_local():\n    import sys\n\n    return sys.platform\n\n\n# Call to verify import works inside function\nresult = test_import_local()\nassert isinstance(result, str), 'sys.platform should be a string'\n\n# Verify sys is NOT in global scope after function call\ntry:\n    sys\n    assert False, 'sys should not be in global scope'\nexcept NameError:\n    pass  # Expected: sys is local to the function\n\n\n# === From import inside function ===\ndef test_from_import_local():\n    from typing import Any\n\n    return Any\n\n\nany_result = test_from_import_local()\nassert repr(any_result) == 'typing.Any', 'should return typing.Any'\n\n# Verify Any is NOT in global scope after function call\ntry:\n    Any\n    assert False, 'Any should not be in global scope'\nexcept NameError:\n    pass  # Expected: Any is local to the function\n\n\n# === Aliased import inside function ===\ndef test_aliased_import_local():\n    import sys as system\n\n    return system.platform\n\n\nalias_result = test_aliased_import_local()\nassert isinstance(alias_result, str), 'system.platform should be a string'\n\n# Verify system is NOT in global scope\ntry:\n    system\n    assert False, 'system should not be in global scope'\nexcept NameError:\n    pass  # Expected: system is local to the function\n\n# === Global import remains accessible ===\nimport sys as global_sys\n\nassert isinstance(global_sys.platform, str), 'global import should work'\n\n\ndef use_global_import():\n    # This should access the global sys, not create a new local\n    return global_sys.platform\n\n\nassert use_global_import() == global_sys.platform, 'function should access global import'\n"
  },
  {
    "path": "crates/monty/test_cases/import__os.py",
    "content": "# call-external\n# Tests for os module import and os.getenv()\n\nimport os\n\n# === os.getenv() with existing variable ===\nassert os.getenv('VIRTUAL_HOME') == '/virtual/home', 'getenv returns existing value'\nassert os.getenv('VIRTUAL_USER') == 'testuser', 'getenv returns user value'\nassert os.getenv('VIRTUAL_EMPTY') == '', 'getenv returns empty string value'\n\n# === os.getenv() with missing variable ===\nassert os.getenv('NONEXISTENT') is None, 'getenv returns None for missing var'\nassert os.getenv('ALSO_MISSING') is None, 'getenv returns None for other missing var'\n\n# === os.getenv() with default value ===\nassert os.getenv('NONEXISTENT', 'fallback') == 'fallback', 'getenv uses default when missing'\nassert os.getenv('ALSO_MISSING', '') == '', 'getenv uses empty string default'\nassert os.getenv('MISSING', None) is None, 'getenv with explicit None default'\n\n# === os.getenv() existing var ignores default ===\nassert os.getenv('VIRTUAL_HOME', 'ignored') == '/virtual/home', 'existing var ignores default'\nassert os.getenv('VIRTUAL_USER', 'other') == 'testuser', 'existing user ignores default'\n\n# === os.getenv() with empty string existing var ===\nassert os.getenv('VIRTUAL_EMPTY', 'not_used') == '', 'empty string var ignores default'\n"
  },
  {
    "path": "crates/monty/test_cases/import__relative_error.py",
    "content": "from .foo import bar\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"import__relative_error.py\", line 1, in <module>\n    from .foo import bar\nImportError: attempted relative import with no known parent package\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/import__relative_no_module_error.py",
    "content": "from . import foo\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"import__relative_no_module_error.py\", line 1, in <module>\n    from . import foo\nImportError: attempted relative import with no known parent package\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/import__runtime_error_when_executed.py",
    "content": "# Verify that ModuleNotFoundError is raised when an unknown module import is actually executed\n# (not guarded by TYPE_CHECKING)\n\ncondition = True\nif condition:\n    import nonexistent_at_runtime\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"import__runtime_error_when_executed.py\", line 6, in <module>\n    import nonexistent_at_runtime\nModuleNotFoundError: No module named 'nonexistent_at_runtime'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/import__star_error.py",
    "content": "# xfail=cpython\nfrom sys import *\n\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"import__star_error.py\", line 2, in <module>\n    from sys import *\n    ~~~~~~~~~~~~~~~~~\nNotImplementedError: Wildcard imports (`from ... import *`) are not supported\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/import__sys.py",
    "content": "# Tests for sys module import\n\nimport sys\n\n# === sys.version ===\n# Check that version is a non-empty string (exact value differs between interpreters)\nassert isinstance(sys.version, str), 'version should be a string'\nassert len(sys.version) > 0, 'version should be non-empty'\n\n# === sys.version_info ===\n# Test index access returns integers for first 3 elements\nassert isinstance(sys.version_info[0], int), 'major version should be int'\nassert isinstance(sys.version_info[1], int), 'minor version should be int'\nassert isinstance(sys.version_info[2], int), 'micro version should be int'\nassert isinstance(sys.version_info[3], str), 'releaselevel should be str'\nassert isinstance(sys.version_info[4], int), 'serial should be int'\n\n# Test negative indexing\nassert sys.version_info[-1] == sys.version_info[4], 'negative index -1 should equal index 4'\nassert sys.version_info[-2] == sys.version_info[3], 'negative index -2 should equal index 3'\nassert sys.version_info[-5] == sys.version_info[0], 'negative index -5 should equal index 0'\n\n# Test named attribute access matches index access\nassert sys.version_info.major == sys.version_info[0], 'major attr should equal index 0'\nassert sys.version_info.minor == sys.version_info[1], 'minor attr should equal index 1'\nassert sys.version_info.micro == sys.version_info[2], 'micro attr should equal index 2'\nassert sys.version_info.releaselevel == sys.version_info[3], 'releaselevel attr should equal index 3'\nassert sys.version_info.serial == sys.version_info[4], 'serial attr should equal index 4'\n\n# Test len\nassert len(sys.version_info) == 5, 'version_info should have 5 elements'\n\n# Test tuple equality (works after fixing NamedTuple equality)\nv = sys.version_info\nassert (v[0], v[1]) == (v.major, v.minor), 'tuple of indices should equal tuple of attrs'\nassert v.major == v[0], 'major attr should equal index 0'\nassert v.minor == v[1], 'minor attr should equal index 1'\n\n# === sys.platform ===\n# Check that platform is a non-empty string (exact value differs between interpreters)\nassert isinstance(sys.platform, str), 'platform should be a string'\nassert len(sys.platform) > 0, 'platform should be non-empty'\n\n# === sys.stdout and sys.stderr ===\n# These should exist - we test by accessing them (will fail if not present)\nstdout = sys.stdout\nstderr = sys.stderr\n"
  },
  {
    "path": "crates/monty/test_cases/import__sys_monty.py",
    "content": "# xfail=cpython\n# Tests for Monty-specific sys module values\n\nimport sys\n\n# === sys.version ===\nassert sys.version == '3.14.0 (Monty)', f'version should be 3.14.0 (Monty), got {sys.version!r}'\n\n# === sys.version_info exact values ===\nassert sys.version_info[0] == 3, 'major version should be 3'\nassert sys.version_info[1] == 14, 'minor version should be 14'\nassert sys.version_info[2] == 0, 'micro version should be 0'\nassert sys.version_info[3] == 'final', 'releaselevel should be final'\nassert sys.version_info[4] == 0, 'serial should be 0'\n\n# === sys.version_info named attributes ===\nassert sys.version_info.major == 3, 'major attr should be 3'\nassert sys.version_info.minor == 14, 'minor attr should be 14'\nassert sys.version_info.micro == 0, 'micro attr should be 0'\nassert sys.version_info.releaselevel == 'final', 'releaselevel attr should be final'\nassert sys.version_info.serial == 0, 'serial attr should be 0'\n\n# === sys.version_info tuple equality ===\n# This works because NamedTuple equality compares only by elements, not type_name\nassert sys.version_info == (3, 14, 0, 'final', 0), 'version_info should equal tuple'\n\n# === sys.platform ===\nassert sys.platform == 'monty', f'platform should be monty, got {sys.platform!r}'\n"
  },
  {
    "path": "crates/monty/test_cases/import__type_checking_guard.py",
    "content": "# === TYPE_CHECKING guard ===\n# Imports inside TYPE_CHECKING blocks should not raise errors at runtime\n# because TYPE_CHECKING is False at runtime, so the import is never executed.\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    import also_nonexistent\n    from nonexistent_module import something\n\n# Verify TYPE_CHECKING is False at runtime (as expected)\nassert TYPE_CHECKING is False, 'TYPE_CHECKING should be False at runtime'\n\n\n# === Function using TYPE_CHECKING for conditional import ===\ndef get_type_checking_value():\n    if TYPE_CHECKING:\n        from another_fake_module import FakeType\n    return 'success'\n\n\nresult = get_type_checking_value()\nassert result == 'success', 'function with TYPE_CHECKING guard should execute'\n\n# === Nested TYPE_CHECKING blocks ===\nif TYPE_CHECKING:\n    if True:\n        from deeply_nested_fake import DeepFake\n\n# === TYPE_CHECKING in else branch (should not be executed either) ===\nx = True\nif x:\n    pass\nelse:\n    if TYPE_CHECKING:\n        from unreachable_module import Unreachable\n\nassert True, 'all TYPE_CHECKING guards work correctly'\n"
  },
  {
    "path": "crates/monty/test_cases/import__typing.py",
    "content": "# === Typing markers via from import ===\nfrom typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union\n\n# These are now assigned to Marker values (not silently ignored)\n# Test repr() to verify they have the correct string representation\nassert repr(Any) == 'typing.Any', f'Any repr should be Any, got {Any!r}'\nassert repr(Optional) == 'typing.Optional', f'Optional repr should be Optional, got {Optional!r}'\nassert repr(Union) == \"<class 'typing.Union'>\", f'Union repr should be <class typing.Union>, got {Union!r}'\nassert repr(List) == 'typing.List', f'List repr should be List, got {List!r}'\nassert repr(Dict) == 'typing.Dict', f'Dict repr should be Dict, got {Dict!r}'\nassert repr(Tuple) == 'typing.Tuple', f'Tuple repr should be Tuple, got {Tuple!r}'\nassert repr(Set) == 'typing.Set', f'Set repr should be Set, got {Set!r}'\nassert repr(Callable) == 'typing.Callable', f'Callable repr should be Callable, got {Callable!r}'\n\n# === Typing markers via module import ===\nimport typing\n\nassert repr(typing.Any) == 'typing.Any'\nassert repr(typing.Optional) == 'typing.Optional'\nassert repr(typing.Union) == \"<class 'typing.Union'>\"\n\n# === Aliased imports ===\nfrom typing import Any as AnyType\n\nassert repr(AnyType) == 'typing.Any'\n"
  },
  {
    "path": "crates/monty/test_cases/import__typing_type_ignore.py",
    "content": "from typing import TYPE_CHECKING\n\nassert TYPE_CHECKING == False, 'TYPE_CHECKING should be False'\nassert not TYPE_CHECKING, 'TYPE_CHECKING should be falsy'\n"
  },
  {
    "path": "crates/monty/test_cases/int__bigint.py",
    "content": "# Tests for BigInt (arbitrary precision integer) support\n\n# === Setup constants ===\nMAX_I64 = 9223372036854775807  # i64::MAX\nMIN_I64 = -MAX_I64 - 1  # i64::MIN (compute to avoid type checker overflow)\n\n# === BigInt literals ===\n# Monty supports parsing integer literals larger than i64\nLITERAL_BIG = 10000000000000000000000000000000000000000\nassert LITERAL_BIG == 10**40, 'bigint literal equals computed value'\nassert str(LITERAL_BIG) == '10000000000000000000000000000000000000000', 'bigint literal str'\nassert type(LITERAL_BIG) == int, 'bigint literal type is int'\n\n# Negative bigint literal (via unary negation)\nNEG_BIG_LITERAL = -10000000000000000000000000000000000000000\nassert NEG_BIG_LITERAL == -(10**40), 'negative bigint literal'\nassert str(NEG_BIG_LITERAL) == '-10000000000000000000000000000000000000000', 'negative bigint literal str'\n\n# === BigInt literal arithmetic ===\n# bigint_literal * int\nassert 10000000000000000000000000000000000000000 * 2 == 2 * 10**40, 'bigint literal * int'\nassert 2 * 10000000000000000000000000000000000000000 == 2 * 10**40, 'int * bigint literal'\n\n# bigint_literal / int (true division)\nassert 10000000000000000000000000000000000000000 / 2 == 10**40 / 2, 'bigint literal / int'\nassert 10000000000000000000000000000000000000000 / 10000000000000000000000000000000000000000 == 1.0, (\n    'bigint literal / bigint literal'\n)\n\n# bigint_literal // int (floor division)\nassert 10000000000000000000000000000000000000000 // 3 == 10**40 // 3, 'bigint literal // int'\nassert 10000000000000000000000000000000000000000 // 10000000000000000000000000000000000000000 == 1, (\n    'bigint literal // bigint literal'\n)\n\n# bigint_literal % int (modulo)\nassert 10000000000000000000000000000000000000000 % 7 == 10**40 % 7, 'bigint literal % int'\nassert 10000000000000000000000000000000000000001 % 10000000000000000000000000000000000000000 == 1, (\n    'bigint literal % bigint literal'\n)\n\n# bigint_literal + int\nassert 10000000000000000000000000000000000000000 + 1 == 10**40 + 1, 'bigint literal + int'\nassert 1 + 10000000000000000000000000000000000000000 == 10**40 + 1, 'int + bigint literal'\n\n# bigint_literal - int\nassert 10000000000000000000000000000000000000000 - 1 == 10**40 - 1, 'bigint literal - int'\nassert 10000000000000000000000000000000000000001 - 10000000000000000000000000000000000000000 == 1, (\n    'bigint literal - bigint literal'\n)\n\n# bigint_literal ** int\nassert 10000000000000000000**2 == 10**38, 'bigint literal ** 2'\n\n# === int() parsing of big integers ===\nassert int('10000000000000000000000000000000000000000') == 10**40, 'int() parses bigint string'\nassert int('-10000000000000000000000000000000000000000') == -(10**40), 'int() parses negative bigint string'\nassert int('99999999999999999999999999999999999999999999999999') == 10**50 - 1, 'int() parses very large bigint string'\n\n# === BigInt literal comparisons ===\nassert 10000000000000000000000000000000000000000 > 9999999999999999999999999999999999999999, (\n    'bigint literal > bigint literal'\n)\nassert 10000000000000000000000000000000000000000 >= 10000000000000000000000000000000000000000, (\n    'bigint literal >= bigint literal'\n)\nassert 9999999999999999999999999999999999999999 < 10000000000000000000000000000000000000000, (\n    'bigint literal < bigint literal'\n)\nassert 10000000000000000000000000000000000000000 <= 10000000000000000000000000000000000000000, (\n    'bigint literal <= bigint literal'\n)\nassert 10000000000000000000000000000000000000000 == 10000000000000000000000000000000000000000, (\n    'bigint literal == bigint literal'\n)\nassert 10000000000000000000000000000000000000000 != 10000000000000000000000000000000000000001, (\n    'bigint literal != bigint literal'\n)\n\n# bigint literal vs int comparisons\nassert 10000000000000000000000000000000000000000 > 1, 'bigint literal > int'\nassert 1 < 10000000000000000000000000000000000000000, 'int < bigint literal'\n\n# === BigInt literal bool conversion ===\nassert bool(10000000000000000000000000000000000000000), 'bigint literal is truthy'\nassert bool(-10000000000000000000000000000000000000000), 'negative bigint literal is truthy'\n\n# === BigInt literal hash consistency ===\n# Same literal value should have same hash\nh1 = hash(10000000000000000000000000000000000000000)\nh2 = hash(10000000000000000000000000000000000000000)\nassert h1 == h2, 'same bigint literal has same hash'\n\n# Computed equal value should have same hash\nh3 = hash(10**40)\nassert h1 == h3, 'bigint literal hash equals computed hash'\n\n# === BigInt literal bitwise operations ===\nassert 10000000000000000000000000000000000000000 & 0xFF == (10**40) & 0xFF, 'bigint literal & int'\nassert 10000000000000000000000000000000000000000 | 1 == (10**40) | 1, 'bigint literal | int'\nassert 10000000000000000000000000000000000000000 ^ 10000000000000000000000000000000000000000 == 0, (\n    'bigint literal ^ bigint literal'\n)\nassert 10000000000000000000000000000000000000000 >> 10 == (10**40) >> 10, 'bigint literal >> int'\nassert 10000000000000000000000000000000000000000 << 10 == (10**40) << 10, 'bigint literal << int'\n\n# === Non-decimal BigInt literals ===\n# Large hex literal (2^64)\nbig_hex = 0x10000000000000000\nassert big_hex == 2**64, 'large hex literal'\n\nbigger_hex = 0x10000000000000000123\nassert bigger_hex == 75557863725914323419427, f'large hex literal {bigger_hex}'\n\n# Large binary literal (2^65)\nbig_bin = 0b100000000000000000000000000000000000000000000000000000000000000000\nassert big_bin == 2**65, 'large binary literal'\n\n# Large octal literal\nbig_oct = 0o10000000000000000000000\nassert big_oct == 8**22, 'large octal literal'\n\n# Underscores in large non-decimal\nbig_hex_underscore = 0x1_0000_0000_0000_0000\nassert big_hex_underscore == 2**64, 'large hex with underscores'\n\n# === BigInt literal in collections ===\nd = {10000000000000000000000000000000000000000: 'value'}\nassert d[10000000000000000000000000000000000000000] == 'value', 'bigint literal as dict key'\nassert d[10**40] == 'value', 'computed bigint finds literal key'\n\nlst = [10000000000000000000000000000000000000000, 20000000000000000000000000000000000000000]\nassert lst[0] == 10**40, 'bigint literal in list'\nassert lst[1] == 2 * 10**40, 'bigint literal in list index 1'\n\n# === BigInt literal repr/str ===\nassert repr(10000000000000000000000000000000000000000) == '10000000000000000000000000000000000000000', (\n    'bigint literal repr'\n)\nassert str(10000000000000000000000000000000000000000) == '10000000000000000000000000000000000000000', (\n    'bigint literal str'\n)\n\n# === Overflow promotion ===\nbigger = MAX_I64 + 1\nassert bigger == MAX_I64 + 1, 'add overflow promotes to bigint'\nassert bigger - 1 == MAX_I64, 'sub back to i64'\n\n# === Subtraction overflow ===\nsmaller = MIN_I64 - 1\nassert smaller == MIN_I64 - 1, 'sub overflow promotes to bigint'\nassert smaller + 1 == MIN_I64, 'add back to i64'\n\n# === Multiplication overflow ===\nmul_result = MAX_I64 * 2\nexpected_mul = MAX_I64 + MAX_I64\nassert mul_result == expected_mul, 'mul overflow'\ntrillion = 1000000000000\ntrillion_squared = trillion * trillion\nassert trillion_squared == 1000000000000 * 1000000000000, 'large mul'\n\n# === Power overflow ===\npow_2_63 = 2**63\nassert pow_2_63 == MAX_I64 + 1, 'pow creates bigint at boundary'\npow_2_64 = 2**64\nassert pow_2_64 == pow_2_63 * 2, 'pow overflow'\npow_2_100 = 2**100\nassert pow_2_100 > pow_2_64, 'large pow is greater'\n\n# === Negative overflow ===\nneg_bigger = -MAX_I64 - 2\nassert neg_bigger == MIN_I64 - 1, 'negative bigint'\n\n# === Type is still int ===\nassert type(bigger) == int, 'bigint type is int'\nassert type(pow_2_100) == int, 'large pow type is int'\n\n# === Mixed operations ===\nadd_result = bigger + 100\nassert add_result == MAX_I64 + 101, 'bigint + int'\nadd_result2 = 100 + bigger\nassert add_result2 == MAX_I64 + 101, 'int + bigint'\nsub_result = bigger - 100\nassert sub_result == MAX_I64 - 99, 'bigint - int'\nsub_result2 = 100 - bigger\nexpected_sub = -(MAX_I64 - 99)\nassert sub_result2 == expected_sub, 'int - bigint'\nmul_result2 = bigger * 2\nexpected_mul2 = (MAX_I64 + 1) * 2\nassert mul_result2 == expected_mul2, 'bigint * int'\nmul_result3 = 2 * bigger\nassert mul_result3 == expected_mul2, 'int * bigint'\n\n# === BigInt with BigInt operations ===\nbig_a = 2**100\nbig_b = 2**100\nbig_sum = big_a + big_b\nassert big_sum == 2**101, 'bigint + bigint'\nbig_diff = big_a - big_b\nassert big_diff == 0, 'bigint - bigint'\nbig_prod = big_a * big_b\nassert big_prod == 2**200, 'bigint * bigint'\n\n# === Comparisons ===\nassert bigger > MAX_I64, 'bigint > int'\nassert MAX_I64 < bigger, 'int < bigint'\nassert bigger >= MAX_I64, 'bigint >= int'\nassert MAX_I64 <= bigger, 'int <= bigint'\ncmp_result = bigger == MAX_I64 + 1\nassert cmp_result, 'bigint == computed int'\ncmp_result2 = bigger == MAX_I64\nassert not cmp_result2, 'bigint != int'\n\n# === BigInt comparisons ===\nassert big_a == big_b, 'bigint == bigint'\ncmp_lt = big_a < big_b\nassert not cmp_lt, 'bigint not < equal bigint'\nbig_double = big_a * 2\nassert big_double > big_b, 'larger bigint > smaller bigint'\n\n# === Hash consistency ===\n# When a BigInt demotes to i64 range, its hash must match the equivalent int hash\n# This is critical for dict key lookups to work correctly\n\n# Test hash equality for values that fit in i64\ncomputed_42 = (big_a - big_a) + 42  # Goes through BigInt arithmetic, demotes to 42\nassert hash(computed_42) == hash(42), 'hash of computed int must match literal int'\nassert hash(bigger - 1) == hash(MAX_I64), 'hash of demoted bigint must match MAX_I64'\nassert hash(smaller + 1) == hash(MIN_I64), 'hash of demoted bigint must match MIN_I64'\n\n# Test that hash(0) is consistent across computation paths\nzero_via_bigint = big_a - big_a\nassert hash(zero_via_bigint) == hash(0), 'hash of bigint zero must match int zero'\n\n# Test dict key lookup works when inserting with int and looking up with computed bigint\nd = {42: 'a'}\nassert d[42] == 'a', 'int as key'\nassert d[computed_42] == 'a', 'lookup with computed bigint finds int key'\n\n# Test dict key lookup works when inserting with bigint and looking up with int\nd2 = {computed_42: 'value'}\nassert d2[42] == 'value', 'lookup with int finds bigint key'\n\n# Large bigints (outside i64 range) as dict keys\nd[bigger] = 'b'\nassert d[bigger] == 'b', 'bigint as key'\nd[big_a] = 'c'\nassert d[big_a] == 'c', 'large bigint as key'\n\n# Verify large bigints with same value hash the same\nbig_copy = 2**100\nassert hash(big_a) == hash(big_copy), 'equal large bigints must hash the same'\n\n# Verify large bigints can be used interchangeably as dict keys\nd3 = {big_a: 'original'}\nassert d3[big_copy] == 'original', 'lookup with equal large bigint works'\n\n# === Unary neg overflow ===\n# Use 0 - MIN_I64 instead of -MIN_I64 to avoid type checker overflow\nneg_min = 0 - MIN_I64\nassert neg_min == MAX_I64 + 1, 'neg i64::MIN promotes'\n\n# Note: ~bigger (bitwise not) tests skipped - Monty parser doesn't support ~ yet\n\n# === Floor division ===\nfd_result = bigger // 2\nfd_expected = (MAX_I64 + 1) // 2\nassert fd_result == fd_expected, 'bigint // int'\npow_2_50 = 2**50\nfd_result2 = pow_2_100 // pow_2_50\nassert fd_result2 == 2**50, 'bigint // bigint'\nfd_result3 = 100 // bigger\nassert fd_result3 == 0, 'int // bigint (small / large)'\nneg_bigger = -bigger\nfd_neg_result = neg_bigger // 3\nfd_neg_expected = (-(MAX_I64 + 1)) // 3\nassert fd_neg_result == fd_neg_expected, 'negative bigint floordiv'\n\n# === Modulo ===\nmod_result = bigger % 1000\nmod_expected = (MAX_I64 + 1) % 1000\nassert mod_result == mod_expected, 'bigint % int'\nmod_result2 = 100 % bigger\nassert mod_result2 == 100, 'int % bigint'\nmod_result3 = pow_2_100 % (pow_2_50 + 1)\nassert mod_result3 == 1, 'bigint % bigint'\n\n# === Builtin functions ===\nabs_neg = abs(-bigger)\nassert abs_neg == bigger, 'abs of negative bigint'\nabs_pos = abs(bigger)\nassert abs_pos == bigger, 'abs of positive bigint'\nabs_min = abs(MIN_I64)\nassert abs_min == MAX_I64 + 1, 'abs of i64::MIN'\n\npow_result = pow(2, 100)\nassert pow_result == pow_2_100, 'pow builtin'\npow_bigger_2 = bigger * bigger\npow_result2 = pow(bigger, 2)\nassert pow_result2 == pow_bigger_2, 'pow with bigint base'\n\ndm = divmod(bigger, 1000)\ndm_quot = dm[0]\ndm_rem = dm[1]\nexpected_quot = bigger // 1000\nexpected_rem = bigger % 1000\nassert dm_quot == expected_quot, 'divmod quotient with bigint'\nassert dm_rem == expected_rem, 'divmod remainder with bigint'\ndm2 = divmod(pow_2_100, pow_2_50)\nassert dm2[0] == pow_2_50, 'divmod bigint by bigint quotient'\nassert dm2[1] == 0, 'divmod bigint by bigint remainder'\n\nhex_result = hex(bigger)\nassert hex_result == '0x8000000000000000', 'hex of bigint'\nhex_neg = hex(-bigger)\nassert hex_neg == '-0x8000000000000000', 'hex of negative bigint'\n\nbin_result = bin(bigger)\nassert bin_result == '0b1000000000000000000000000000000000000000000000000000000000000000', 'bin of bigint'\nbin_neg = bin(-bigger)\nassert bin_neg == '-0b1000000000000000000000000000000000000000000000000000000000000000', 'bin of negative bigint'\n\noct_result = oct(bigger)\nassert oct_result == '0o1000000000000000000000', 'oct of bigint'\noct_neg = oct(-bigger)\nassert oct_neg == '-0o1000000000000000000000', 'oct of negative bigint'\n\n# === Repr and str ===\nrepr_result = repr(bigger)\nstr_result = str(bigger)\nexpected_repr = str(MAX_I64 + 1)\nassert repr_result == expected_repr, 'repr of bigint'\nassert str_result == expected_repr, 'str of bigint'\n\n# === Bool conversion ===\nassert bool(bigger), 'bigint is truthy'\nassert bool(-bigger), 'negative bigint is truthy'\n\n# === Demote back to i64 ===\ndemote_result = bigger - bigger\nassert demote_result == 0, 'bigint - bigint can demote to i64'\ndemote_result2 = bigger - 1\nassert demote_result2 == MAX_I64, 'bigint - 1 demotes to i64::MAX'\n\n# === Bug 1: 0 ** 0 with LongInt exponent ===\nbig = 2**100\nassert 0**big == 0, '0 ** large_positive should be 0'\nassert 1**big == 1, '1 ** large_positive should be 1'\n# Edge case: 0 ** 0 where 0 is a LongInt\nzero_big = big - big  # LongInt zero (actually demotes to int, so test with computed zero)\nassert 0**zero_big == 1, '0 ** 0 (computed zero) should be 1'\nassert 5**zero_big == 1, '5 ** 0 (computed zero) should be 1'\n\n# === Bug 2: Modulo with negative divisor ===\nassert 5 % -3 == -1, '5 % -3 should be -1'\nassert -5 % 3 == 1, '-5 % 3 should be 1'\nassert -5 % -3 == -2, '-5 % -3 should be -2'\nassert 7 % -4 == -1, '7 % -4 should be -1'\n\n# === Bug 3: += overflow ===\nx = MAX_I64\nx += 1\nassert x == MAX_I64 + 1, 'i64::MAX += 1 should promote to LongInt'\ny = MIN_I64\ny += -1\nassert y == MIN_I64 - 1, 'i64::MIN += -1 should promote to LongInt'\n\n# === Bug 4: LongInt * sequence ===\nbig = 2**100\nassert 'a' * 0 == '', 'str * 0'\nassert [1] * 0 == [], 'list * 0'\n# Sequence * LongInt (where LongInt is heap-allocated)\n# Note: CPython doesn't support seq * huge_negative_longint (OverflowError)\n# Test with positive LongInt - should raise OverflowError for repeat count too large\n# But we can test heap-allocated LongInt by using a value that demotes\nbig_then_small = big - big + 3  # Results in 3 (goes through LongInt arithmetic)\nassert 'ab' * big_then_small == 'ababab', 'str * LongInt that demotes to small value'\n\n# === Bug 5: True division with LongInt ===\nbig = 2**100\nassert big / 2 == 2.0**99, 'bigint / int'\n# 1 / 2**100 is a very small positive number, not exactly 0.0\ntiny = 1 / big\nassert tiny > 0.0 and tiny < 1e-29, 'int / huge_bigint approaches 0'\nassert big / big == 1.0, 'bigint / bigint same value'\nassert big / 2.0 == 2.0**99, 'bigint / float'\ntiny_f = 1.0 / big\nassert tiny_f > 0.0 and tiny_f < 1e-29, 'float / huge_bigint approaches 0'\n\n# === Bug 6: Bitwise with LongInt ===\nbig = 2**100\nassert big & 0xFF == 0, '2**100 & 0xFF'\nassert big | 1 == big + 1, '2**100 | 1'\nassert big ^ big == 0, 'bigint ^ same bigint'\nassert big >> 50 == 2**50, '2**100 >> 50'\nassert 1 << 100 == big, '1 << 100'\nassert (big + 0xFF) & 0xFF == 0xFF, 'bigint with low bits & mask'\n\n# === Large result operations (should succeed with NoLimitTracker) ===\n# These are large but allowed since test runner uses NoLimitTracker\nx = 2**100000  # ~12.5KB - well under any reasonable limit\nassert x > 0, '2 ** 100000 should succeed'\n\ny = 1 << 100000\nassert y > 0, '1 << 100000 should succeed'\n\n# Edge cases (constant-size results) - always succeed\nassert 0**10000000 == 0, '0 ** huge = 0'\nassert 1**10000000 == 1, '1 ** huge = 1'\nassert (-1) ** 10000000 == 1, '(-1) ** huge_even = 1'\nassert (-1) ** 10000001 == -1, '(-1) ** huge_odd = -1'\nassert 0 << 10000000 == 0, '0 << huge = 0'\n\n# === LongInt in range() ===\n# Note: Monty raises OverflowError immediately for range(10**100), while CPython\n# only raises when iterating or calling len(). We accept this difference for safety.\nbig = 2**100\nsmall_via_big = big - big + 5  # LongInt that demotes to 5\nr = range(small_via_big)\nassert list(r) == [0, 1, 2, 3, 4], 'range with LongInt stop'\n\nr2 = range(small_via_big, small_via_big + 3)\nassert list(r2) == [5, 6, 7], 'range with LongInt start/stop'\n\nr3 = range(0, 10, big - big + 2)\nassert list(r3) == [0, 2, 4, 6, 8], 'range with LongInt step'\n\n# === Integer computed via LongInt arithmetic ===\n# These values go through BigInt arithmetic but demote to regular Int via into_value()\nidx = big - big + 1  # Results in Value::Int(1) after demotion\nassert [10, 20, 30][idx] == 20, 'list indexing with BigInt-computed int'\nassert (10, 20, 30)[idx] == 20, 'tuple indexing with BigInt-computed int'\nassert 'abc'[idx] == 'b', 'string indexing with BigInt-computed int'\nassert b'abc'[idx] == ord('b'), 'bytes indexing with BigInt-computed int'\nassert range(10)[idx] == 1, 'range indexing with BigInt-computed int'\n\n# Negative index computed via LongInt arithmetic\nneg_idx = big - big - 1  # Results in Value::Int(-1) after demotion\nassert [10, 20, 30][neg_idx] == 30, 'list indexing with negative BigInt-computed int'\nassert (10, 20, 30)[neg_idx] == 30, 'tuple indexing with negative BigInt-computed int'\nassert 'abc'[neg_idx] == 'c', 'string indexing with negative BigInt-computed int'\nassert b'abc'[neg_idx] == ord('c'), 'bytes indexing with negative BigInt-computed int'\nassert range(10)[neg_idx] == 9, 'range indexing with negative BigInt-computed int'\n\n# List assignment with LongInt index\nlst = [1, 2, 3]\nlst[idx] = 42\nassert lst == [1, 42, 3], 'list assignment with BigInt-computed index'\nlst[neg_idx] = 99\nassert lst == [1, 42, 99], 'list assignment with negative BigInt-computed index'\n\n# === String/bytes * LongInt ===\ncount = big - big + 3\nassert 'ab' * count == 'ababab', 'string * LongInt'\nassert count * 'ab' == 'ababab', 'LongInt * string'\nassert b'ab' * count == b'ababab', 'bytes * LongInt'\nassert count * b'ab' == b'ababab', 'LongInt * bytes'\n\n# Negative LongInt repeat\nneg = big - big - 2\nassert 'ab' * neg == '', 'string * negative LongInt'\nassert b'ab' * neg == b'', 'bytes * negative LongInt'\n\n# Zero LongInt repeat\nzero = big - big\nassert 'ab' * zero == '', 'string * zero LongInt'\nassert b'ab' * zero == b'', 'bytes * zero LongInt'\n"
  },
  {
    "path": "crates/monty/test_cases/int__bigint_errors.py",
    "content": "# Tests for error cases in BigInt-related builtins and operations\n# All error messages must match CPython exactly\n# Uses 'in str(e)' checks since Monty's str(e) includes the type name\n\n# === Setup constants ===\nMAX_I64 = 9223372036854775807\nBIGINT = MAX_I64 + 1  # Force BigInt creation\n\n\n# === hex() errors ===\ntry:\n    hex('str')\n    assert False, 'hex(str) should raise TypeError'\nexcept TypeError as e:\n    assert \"'str' object cannot be interpreted as an integer\" in str(e), f'hex str error: {e}'\n\ntry:\n    hex(1.5)\n    assert False, 'hex(float) should raise TypeError'\nexcept TypeError as e:\n    assert \"'float' object cannot be interpreted as an integer\" in str(e), f'hex float error: {e}'\n\ntry:\n    hex([])\n    assert False, 'hex(list) should raise TypeError'\nexcept TypeError as e:\n    assert \"'list' object cannot be interpreted as an integer\" in str(e), f'hex list error: {e}'\n\n\n# === bin() errors ===\ntry:\n    bin('str')\n    assert False, 'bin(str) should raise TypeError'\nexcept TypeError as e:\n    assert \"'str' object cannot be interpreted as an integer\" in str(e), f'bin str error: {e}'\n\ntry:\n    bin(1.5)\n    assert False, 'bin(float) should raise TypeError'\nexcept TypeError as e:\n    assert \"'float' object cannot be interpreted as an integer\" in str(e), f'bin float error: {e}'\n\ntry:\n    bin({})\n    assert False, 'bin(dict) should raise TypeError'\nexcept TypeError as e:\n    assert \"'dict' object cannot be interpreted as an integer\" in str(e), f'bin dict error: {e}'\n\n\n# === oct() errors ===\ntry:\n    oct('str')\n    assert False, 'oct(str) should raise TypeError'\nexcept TypeError as e:\n    assert \"'str' object cannot be interpreted as an integer\" in str(e), f'oct str error: {e}'\n\ntry:\n    oct(1.5)\n    assert False, 'oct(float) should raise TypeError'\nexcept TypeError as e:\n    assert \"'float' object cannot be interpreted as an integer\" in str(e), f'oct float error: {e}'\n\ntry:\n    oct((1, 2))\n    assert False, 'oct(tuple) should raise TypeError'\nexcept TypeError as e:\n    assert \"'tuple' object cannot be interpreted as an integer\" in str(e), f'oct tuple error: {e}'\n\n\n# === divmod() division by zero ===\ntry:\n    divmod(10, 0)\n    assert False, 'divmod(int, 0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod int/0 error: {e}'\n\ntry:\n    divmod(BIGINT, 0)\n    assert False, 'divmod(bigint, 0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod bigint/0 error: {e}'\n\ntry:\n    divmod(10, BIGINT - BIGINT)  # BigInt zero\n    assert False, 'divmod(int, bigint_zero) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod int/bigint_zero error: {e}'\n\ntry:\n    divmod(BIGINT, BIGINT - BIGINT)  # BigInt / BigInt zero\n    assert False, 'divmod(bigint, bigint_zero) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod bigint/bigint_zero error: {e}'\n\ntry:\n    divmod(10.0, 0.0)\n    assert False, 'divmod(float, 0.0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod float/0.0 error: {e}'\n\ntry:\n    divmod(10, 0.0)\n    assert False, 'divmod(int, 0.0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod int/0.0 error: {e}'\n\ntry:\n    divmod(10.0, 0)\n    assert False, 'divmod(float, 0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'divmod float/0 error: {e}'\n\n\n# === divmod() type errors ===\ntry:\n    divmod('a', 5)\n    assert False, 'divmod(str, int) should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for divmod(): 'str' and 'int'\" in str(e), f'divmod str/int error: {e}'\n\ntry:\n    divmod(5, 'a')\n    assert False, 'divmod(int, str) should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for divmod(): 'int' and 'str'\" in str(e), f'divmod int/str error: {e}'\n\ntry:\n    divmod([], 5)\n    assert False, 'divmod(list, int) should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for divmod(): 'list' and 'int'\" in str(e), f'divmod list/int error: {e}'\n\ntry:\n    divmod(BIGINT, 'a')\n    assert False, 'divmod(bigint, str) should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for divmod(): 'int' and 'str'\" in str(e), f'divmod bigint/str error: {e}'\n\n\n# === pow() zero to negative power ===\ntry:\n    pow(0.0, -1)\n    assert False, 'pow(0.0, -1) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'zero to a negative power' in str(e), f'pow 0.0/-1 error: {e}'\n\ntry:\n    pow(0, -1.0)\n    assert False, 'pow(0, -1.0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'zero to a negative power' in str(e), f'pow 0/-1.0 error: {e}'\n\ntry:\n    pow(0.0, -2.0)\n    assert False, 'pow(0.0, -2.0) should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'zero to a negative power' in str(e), f'pow 0.0/-2.0 error: {e}'\n\n\n# === pow() with modulo errors ===\ntry:\n    pow(2, 10, 0)\n    assert False, 'pow(2, 10, 0) should raise ValueError'\nexcept ValueError as e:\n    assert 'pow() 3rd argument cannot be 0' in str(e), f'pow mod=0 error: {e}'\n\n# Note: pow(2, -1, 5) computes modular inverse in Python 3.8+, not an error\n# But pow(2, -1, 4) raises an error because 2 is not invertible mod 4\ntry:\n    pow(2, -1, 4)  # gcd(2, 4) != 1, no inverse exists\n    assert False, 'pow(2, -1, 4) should raise ValueError'\nexcept ValueError as e:\n    # CPython: \"base is not invertible for the given modulus\"\n    # Monty: \"pow() 2nd argument cannot be negative when 3rd argument specified\"\n    # Accept either message since Monty doesn't support modular inverse yet\n    assert 'not invertible' in str(e) or 'cannot be negative' in str(e), f'pow non-invertible error: {e}'\n\ntry:\n    pow(2.0, 2, 5)\n    assert False, 'pow(float, int, int) should raise TypeError'\nexcept TypeError as e:\n    assert 'pow() 3rd argument not allowed unless all arguments are integers' in str(e), f'pow float mod error: {e}'\n\ntry:\n    pow(2, 2.0, 5)\n    assert False, 'pow(int, float, int) should raise TypeError'\nexcept TypeError as e:\n    assert 'pow() 3rd argument not allowed unless all arguments are integers' in str(e), f'pow float exp mod error: {e}'\n\ntry:\n    pow(2, 2, 5.0)\n    assert False, 'pow(int, int, float) should raise TypeError'\nexcept TypeError as e:\n    assert 'pow() 3rd argument not allowed unless all arguments are integers' in str(e), f'pow float mod2 error: {e}'\n\n\n# === abs() type errors ===\ntry:\n    abs('str')\n    assert False, 'abs(str) should raise TypeError'\nexcept TypeError as e:\n    assert \"bad operand type for abs(): 'str'\" in str(e), f'abs str error: {e}'\n\ntry:\n    abs([])\n    assert False, 'abs(list) should raise TypeError'\nexcept TypeError as e:\n    assert \"bad operand type for abs(): 'list'\" in str(e), f'abs list error: {e}'\n\ntry:\n    abs({})\n    assert False, 'abs(dict) should raise TypeError'\nexcept TypeError as e:\n    assert \"bad operand type for abs(): 'dict'\" in str(e), f'abs dict error: {e}'\n\n\n# === pow() type errors (** operator) ===\ntry:\n    5 ** 'x'\n    assert False, '5 ** str should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for ** or pow(): 'int' and 'str'\" in str(e), f'int ** str error: {e}'\n\ntry:\n    'x' ** 5\n    assert False, 'str ** int should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for ** or pow(): 'str' and 'int'\" in str(e), f'str ** int error: {e}'\n\ntry:\n    BIGINT ** 'x'\n    assert False, 'bigint ** str should raise TypeError'\nexcept TypeError as e:\n    assert \"unsupported operand type(s) for ** or pow(): 'int' and 'str'\" in str(e), f'bigint ** str error: {e}'\n\n\n# === Division by zero with BigInt ===\ntry:\n    BIGINT // 0\n    assert False, 'bigint // 0 should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'bigint floordiv error: {e}'\n\ntry:\n    BIGINT % 0\n    assert False, 'bigint % 0 should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'bigint mod error: {e}'\n\ntry:\n    10 // (BIGINT - BIGINT)  # int // BigInt zero\n    assert False, 'int // bigint_zero should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'int floordiv bigint_zero error: {e}'\n\ntry:\n    10 % (BIGINT - BIGINT)  # int % BigInt zero\n    assert False, 'int % bigint_zero should raise ZeroDivisionError'\nexcept ZeroDivisionError as e:\n    assert 'division by zero' in str(e), f'int mod bigint_zero error: {e}'\n"
  },
  {
    "path": "crates/monty/test_cases/int__ops.py",
    "content": "# === Integer addition ===\nassert 1 + 2 == 3, 'basic add'\nassert 5 + 0 == 5, 'add zero'\nassert 0 + 5 == 5, 'zero add'\n\n# === Integer subtraction ===\nassert 5 - 3 == 2, 'basic sub'\nassert 5 - 0 == 5, 'sub zero'\n\n# === Mixed int/float addition ===\nassert 3 + 4.0 == 7.0, 'int add float'\nassert 4.0 + 3 == 7.0, 'float add int'\nassert -2 + 3.5 == 1.5, 'neg int add float'\nassert 0 + 2.5 == 2.5, 'zero add float'\nassert 2.5 + 0 == 2.5, 'float add zero'\n\n# === Mixed int/float subtraction ===\nassert 5 - 2.5 == 2.5, 'int sub float'\nassert 5.5 - 2 == 3.5, 'float sub int'\nassert -3 - 1.5 == -4.5, 'neg int sub float'\nassert 1.5 - (-2) == 3.5, 'float sub neg int'\n\n# === Float subtraction ===\nassert 5.5 - 2.5 == 3.0, 'float sub float'\nassert 0.0 - 1.5 == -1.5, 'zero sub float'\n\n# === Integer modulo ===\nassert 10 % 3 == 1, 'basic mod'\nassert 3 % 10 == 3, 'mod larger divisor'\nassert 9 % 3 == 0, 'mod zero result'\n\n# === Augmented assignment (+=) ===\nx = 5\nx += 3\nassert x == 8, 'basic iadd'\n\n# === Integer repr/str ===\nassert repr(42) == '42', 'int repr'\nassert str(42) == '42', 'int str'\n\n# === Float repr/str ===\nassert repr(2.5) == '2.5', 'float repr'\nassert str(2.5) == '2.5', 'float str'\n\n# === Integer multiplication ===\nassert 3 * 4 == 12, 'basic int mult'\nassert 5 * 0 == 0, 'mult by zero'\nassert 0 * 5 == 0, 'zero mult'\nassert -3 * 4 == -12, 'negative mult'\nassert 3 * -4 == -12, 'mult negative'\nassert -3 * -4 == 12, 'neg mult neg'\n\n# === Float multiplication ===\nassert 3.0 * 4.0 == 12.0, 'float mult'\nassert 2.5 * 2.0 == 5.0, 'float mult 2'\n\n# === Mixed int/float multiplication ===\nassert 3 * 4.0 == 12.0, 'int mult float'\nassert 4.0 * 3 == 12.0, 'float mult int'\n\n# === True division (always returns float) ===\nassert 6 / 2 == 3.0, 'int div exact'\nassert 7 / 2 == 3.5, 'int div remainder'\nassert 1 / 4 == 0.25, 'int div fraction'\nassert 6.0 / 2.0 == 3.0, 'float div'\nassert 7 / 2.0 == 3.5, 'int div float'\nassert 7.0 / 2 == 3.5, 'float div int'\nassert -7 / 2 == -3.5, 'neg div'\n\n# === Floor division ===\nassert 7 // 2 == 3, 'int floor div'\nassert 6 // 2 == 3, 'int floor div exact'\nassert -7 // 2 == -4, 'neg floor div rounds down'\nassert 7 // -2 == -4, 'floor div neg rounds down'\nassert -7 // -2 == 3, 'neg floor div neg'\nassert 7.0 // 2.0 == 3.0, 'float floor div'\nassert 7 // 2.0 == 3.0, 'int floor div float'\nassert 7.0 // 2 == 3.0, 'float floor div int'\nassert -7.0 // 2.0 == -4.0, 'neg float floor div'\n\n# === Power (exponentiation) ===\nassert 2**3 == 8, 'int pow'\nassert 2**10 == 1024, 'int pow large'\nassert 2**0 == 1, 'pow zero'\nassert (-2) ** 3 == -8, 'neg base pow'\nassert (-2) ** 2 == 4, 'neg base even pow'\nassert 2**-1 == 0.5, 'pow neg returns float'\nassert 2**-2 == 0.25, 'pow neg 2'\nassert 4.0**2.0 == 16.0, 'float pow'\nassert 4**0.5 == 2.0, 'sqrt via pow'\nassert 8 ** (1 / 3) == 2.0, 'cube root via pow'\nassert 2.0**3 == 8.0, 'float pow int'\n\n# === Augmented assignment operators ===\n# *=\nx = 5\nx *= 3\nassert x == 15, 'imult'\n\n# /=\nx = 10\nx /= 4\nassert x == 2.5, 'idiv'\n\n# //=\nx = 10\nx //= 3\nassert x == 3, 'ifloordiv'\n\n# **=\nx = 2\nx **= 4\nassert x == 16, 'ipow'\n\n# -=\nx = 10\nx -= 3\nassert x == 7, 'isub'\n\n# %=\nx = 10\nx %= 3\nassert x == 1, 'imod'\n\n# === Bool arithmetic (True=1, False=0) ===\n# Bool multiplication\nassert True * 3 == 3, 'bool mult int'\nassert False * 5 == 0, 'false mult int'\nassert 3 * True == 3, 'int mult bool'\nassert 3 * False == 0, 'int mult false'\nassert True * True == 1, 'bool mult bool'\nassert True * False == 0, 'bool mult false'\nassert True * 2.5 == 2.5, 'bool mult float'\nassert 2.5 * True == 2.5, 'float mult bool'\n\n# Bool division\nassert True / 2 == 0.5, 'bool div int'\nassert False / 2 == 0.0, 'false div int'\nassert 4 / True == 4.0, 'int div bool'\nassert True / True == 1.0, 'bool div bool'\nassert True / 2.0 == 0.5, 'bool div float'\nassert 4.0 / True == 4.0, 'float div bool'\n\n# Bool floor division\nassert True // 2 == 0, 'bool floordiv int'\nassert False // 2 == 0, 'false floordiv int'\nassert 5 // True == 5, 'int floordiv bool'\nassert True // True == 1, 'bool floordiv bool'\nassert True // 2.0 == 0.0, 'bool floordiv float'\nassert 5.5 // True == 5.0, 'float floordiv bool'\n\n# Bool power\nassert True**3 == 1, 'bool pow int'\nassert False**3 == 0, 'false pow int'\nassert 2**True == 2, 'int pow bool true'\nassert 2**False == 1, 'int pow bool false'\nassert True**True == 1, 'bool pow bool'\nassert False**False == 1, 'false pow false'\nassert True**2.0 == 1.0, 'bool pow float'\nassert 2.0**True == 2.0, 'float pow bool true'\nassert 2.0**False == 1.0, 'float pow bool false'\n\n# === Unary positive (no-op for numbers, converts bools to int) ===\nassert +5 == 5, 'unary pos int'\nassert +(-3) == -3, 'unary pos neg int'\nassert +0 == 0, 'unary pos zero'\nassert +3.14 == 3.14, 'unary pos float'\nassert +(-2.5) == -2.5, 'unary pos neg float'\nassert +0.0 == 0.0, 'unary pos zero float'\nassert +True == 1, 'unary pos true'\nassert +False == 0, 'unary pos false'\n# Verify +bool returns int type, not bool\nassert type(+True) == int, 'unary pos true returns int type'\nassert type(+False) == int, 'unary pos false returns int type'\n\n# === Unary negative ===\nassert -5 == -5, 'unary neg int'\nassert -(-3) == 3, 'unary neg neg int'\nassert -0 == 0, 'unary neg zero'\nassert -3.14 == -3.14, 'unary neg float'\nassert -(-2.5) == 2.5, 'unary neg neg float'\nassert -True == -1, 'unary neg true'\nassert repr(-True) == '-1', 'unary neg true repr'\nassert -False == 0, 'unary neg false'\nassert repr(-False) == '0', 'unary neg false repr'\n\n# === Unary invert (bitwise NOT) ===\nassert ~0 == -1, 'unary invert zero'\nassert ~1 == -2, 'unary invert one'\nassert ~(-1) == 0, 'unary invert neg one'\nassert ~True == -2, 'unary invert true'\nassert repr(~True) == '-2', 'unary invert true repr'\nassert ~False == -1, 'unary invert false'\nassert repr(~False) == '-1', 'unary invert false repr'\n\nassert int('123') == 123, 'int conversion from string'\nassert int('  123  ') == 123, 'int conversion from string trim'\nassert int('1_234 ') == 1234, 'int conversion from string'\n\ntry:\n    int('abc')\nexcept ValueError as e:\n    assert str(e) == \"invalid literal for int() with base 10: 'abc'\", f'got err: {e}'\nelse:\n    raise AssertionError('int conversion from string should fail')\n\ntry:\n    int(' ')\nexcept ValueError as e:\n    assert str(e) == \"invalid literal for int() with base 10: ' '\", f'got err: {e}'\nelse:\n    raise AssertionError('int conversion from string should fail')\n\ntry:\n    int('a\\tbc')\nexcept ValueError as e:\n    assert str(e) == \"invalid literal for int() with base 10: 'a\\\\tbc'\", f'got err: {e}'\nelse:\n    raise AssertionError('int conversion from string should fail')\n"
  },
  {
    "path": "crates/monty/test_cases/int__overflow_division.py",
    "content": "# === i64::MIN // -1 overflow ===\nINT_MIN = -(2**63)\nINT_MAX = 2**63 - 1\n\nassert INT_MIN // -1 == 9223372036854775808, 'INT_MIN // -1'\nassert INT_MIN % -1 == 0, 'INT_MIN % -1'\n\nq, r = divmod(INT_MIN, -1)\nassert q == 9223372036854775808, 'divmod(INT_MIN, -1) quot'\nassert r == 0, 'divmod(INT_MIN, -1) rem'\n\n# === augmented assignment ===\nx = INT_MIN\nx //= -1\nassert x == 9223372036854775808, 'INT_MIN //= -1'\n\nx = INT_MIN\nx %= -1\nassert x == 0, 'INT_MIN %= -1'\n\n# === i64 boundary values ===\nassert INT_MIN // 1 == INT_MIN, 'INT_MIN // 1'\nassert INT_MIN // -2 == 4611686018427387904, 'INT_MIN // -2'\nassert INT_MIN % 1 == 0, 'INT_MIN % 1'\nassert INT_MIN % -2 == 0, 'INT_MIN % -2'\nassert INT_MIN % 3 == 1, 'INT_MIN % 3'\n\nassert INT_MAX // -1 == -INT_MAX, 'INT_MAX // -1'\nassert INT_MAX % -1 == 0, 'INT_MAX % -1'\nassert INT_MAX // 2 == 4611686018427387903, 'INT_MAX // 2'\nassert INT_MAX % 2 == 1, 'INT_MAX % 2'\n\n# === boundary divisors ===\nassert INT_MIN // INT_MIN == 1, 'INT_MIN // INT_MIN'\nassert INT_MIN // INT_MAX == -2, 'INT_MIN // INT_MAX'\nassert INT_MAX // INT_MIN == -1, 'INT_MAX // INT_MIN'\nassert INT_MIN % INT_MIN == 0, 'INT_MIN % INT_MIN'\nassert INT_MAX % INT_MAX == 0, 'INT_MAX % INT_MAX'\n\n# === sign combinations ===\nassert -7 // 2 == -4, '-7 // 2'\nassert 7 // -2 == -4, '7 // -2'\nassert -7 % 2 == 1, '-7 % 2'\nassert 7 % -2 == -1, '7 % -2'\n\nq, r = divmod(-7, 2)\nassert q == -4, 'divmod(-7, 2) quot'\nassert r == 1, 'divmod(-7, 2) rem'\n\nq, r = divmod(7, -2)\nassert q == -4, 'divmod(7, -2) quot'\nassert r == -1, 'divmod(7, -2) rem'\n\n# === divmod at boundaries ===\nq, r = divmod(INT_MIN, 2)\nassert q == -4611686018427387904, 'divmod(INT_MIN, 2) quot'\nassert r == 0, 'divmod(INT_MIN, 2) rem'\n\nq, r = divmod(INT_MAX, -1)\nassert q == -INT_MAX, 'divmod(INT_MAX, -1) quot'\nassert r == 0, 'divmod(INT_MAX, -1) rem'\n\nq, r = divmod(INT_MIN, INT_MAX)\nassert q == -2, 'divmod(INT_MIN, INT_MAX) quot'\nassert r == INT_MAX - 1, 'divmod(INT_MIN, INT_MAX) rem'\n\n# === divmod invariant: q * b + r == a ===\nq, r = divmod(INT_MIN, -1)\nassert q * -1 + r == INT_MIN, 'divmod(INT_MIN, -1) invariant'\n\nq, r = divmod(INT_MIN, 3)\nassert q * 3 + r == INT_MIN, 'divmod(INT_MIN, 3) invariant'\nassert q == -3074457345618258603, 'divmod(INT_MIN, 3) quot'\nassert r == 1, 'divmod(INT_MIN, 3) rem'\n\n# === CompareModEq patterns ===\nx = INT_MIN\nassert x % -1 == 0, 'INT_MIN % -1 == 0'\nassert x % 2 == 0, 'INT_MIN % 2 == 0'\nassert x % 3 == 1, 'INT_MIN % 3 == 1'\n\nx = INT_MAX\nassert x % -1 == 0, 'INT_MAX % -1 == 0'\nassert x % 2 == 1, 'INT_MAX % 2 == 1'\n"
  },
  {
    "path": "crates/monty/test_cases/is_variant__all.py",
    "content": "# Tests that values of different types are returned correctly\n# Also tests identity operators with singletons\n\n# === Boolean values ===\nassert repr(False) == 'False', 'False repr'\nassert repr(True) == 'True', 'True repr'\n\n# === None value ===\nassert repr(None) == 'None', 'None repr'\n\n# === Ellipsis value ===\nassert repr(...) == 'Ellipsis', 'Ellipsis repr'\n\n# === Ellipsis identity ===\nassert (... is ...) == True, 'ellipsis is ellipsis'\nassert (None is ...) == False, 'none is not ellipsis'\n\n# === Type checks against None ===\nassert (False is None) == False, 'False is not None'\nassert (True is None) == False, 'True is not None'\nassert (None is None) == True, 'None is None'\nassert (42 is None) == False, 'int is not None'\nassert (3.14 is None) == False, 'float is not None'\nassert ([1, 2] is None) == False, 'list is not None'\nassert ('hello' is None) == False, 'str is not None'\nassert ((1, 2) is None) == False, 'tuple is not None'\n\n# === Type checks against Ellipsis ===\nassert (False is ...) == False, 'False is not Ellipsis'\nassert (True is ...) == False, 'True is not Ellipsis'\nassert (None is ...) == False, 'None is not Ellipsis'\nassert (42 is ...) == False, 'int is not Ellipsis'\nassert (3.14 is ...) == False, 'float is not Ellipsis'\nassert ([1, 2] is ...) == False, 'list is not Ellipsis'\nassert ('hello' is ...) == False, 'str is not Ellipsis'\nassert ((1, 2) is ...) == False, 'tuple is not Ellipsis'\n"
  },
  {
    "path": "crates/monty/test_cases/isinstance__arg2_list_error.py",
    "content": "isinstance(1, [int, str])\n# Raise=TypeError('isinstance() arg 2 must be a type, a tuple of types, or a union')\n"
  },
  {
    "path": "crates/monty/test_cases/isinstance__arg2_type_error.py",
    "content": "isinstance(1, 'int')\n# Raise=TypeError('isinstance() arg 2 must be a type, a tuple of types, or a union')\n"
  },
  {
    "path": "crates/monty/test_cases/iter__dict_mutation.py",
    "content": "d = {'a': 1, 'b': 2}\nfor k in d:\n    d['c'] = 3\n# Raise=RuntimeError('dictionary changed size during iteration')\n"
  },
  {
    "path": "crates/monty/test_cases/iter__for.py",
    "content": "# === List iteration ===\nresult = []\nfor x in [1, 2, 3]:\n    result.append(x)\nassert result == [1, 2, 3], 'iterate over list'\n\n# list with mixed types\nresult = []\nfor x in [1, 'a', True]:\n    result.append(x)\nassert result == [1, 'a', True], 'iterate over mixed list'\n\n# empty list\nresult = []\nfor x in []:\n    result.append(x)\nassert result == [], 'iterate over empty list'\n\n# nested list items\nresult = []\nfor x in [[1, 2], [3, 4]]:\n    result.append(x)\nassert result == [[1, 2], [3, 4]], 'iterate over nested lists'\n\n# === Tuple iteration ===\nresult = []\nfor x in (1, 2, 3):\n    result.append(x)\nassert result == [1, 2, 3], 'iterate over tuple'\n\n# empty tuple\nresult = []\nfor x in ():\n    result.append(x)\nassert result == [], 'iterate over empty tuple'\n\n# tuple with mixed types\nresult = []\nfor x in (1, 'b', False):\n    result.append(x)\nassert result == [1, 'b', False], 'iterate over mixed tuple'\n\n# === Dict iteration (yields keys) ===\nresult = []\nfor k in {'a': 1, 'b': 2, 'c': 3}:\n    result.append(k)\nassert result == ['a', 'b', 'c'], 'iterate over dict yields keys'\n\n# empty dict\nresult = []\nfor k in {}:\n    result.append(k)\nassert result == [], 'iterate over empty dict'\n\n# dict preserves insertion order\nresult = []\nd = {'z': 1, 'a': 2, 'm': 3}\nfor k in d:\n    result.append(k)\nassert result == ['z', 'a', 'm'], 'dict iteration preserves insertion order'\n\n# === String iteration (yields chars) ===\nresult = []\nfor c in 'abc':\n    result.append(c)\nassert result == ['a', 'b', 'c'], 'iterate over string yields chars'\n\n# empty string\nresult = []\nfor c in '':\n    result.append(c)\nassert result == [], 'iterate over empty string'\n\n# string with punctuation\nresult = []\nfor c in 'hi!':\n    result.append(c)\nassert result == ['h', 'i', '!'], 'iterate over string with punctuation'\n\n# string with unicode (multi-byte UTF-8 characters)\nresult = []\nfor c in 'héllo':\n    result.append(c)\nassert result == ['h', 'é', 'l', 'l', 'o'], 'iterate over string with accented char'\n\n# string with CJK characters\nresult = []\nfor c in '日本':\n    result.append(c)\nassert result == ['日', '本'], 'iterate over string with CJK chars'\n\n# string with emoji\nresult = []\nfor c in 'a🎉b':\n    result.append(c)\nassert result == ['a', '🎉', 'b'], 'iterate over string with emoji'\n\n# heap string\ns = 'xyz'\ns = s + '!'  # Force heap allocation\nresult = []\nfor c in s:\n    result.append(c)\nassert result == ['x', 'y', 'z', '!'], 'iterate over heap string'\n\n# === Bytes iteration (yields ints) ===\nresult = []\nfor b in b'abc':\n    result.append(b)\nassert result == [97, 98, 99], 'iterate over bytes yields ints'\n\n# empty bytes\nresult = []\nfor b in b'':\n    result.append(b)\nassert result == [], 'iterate over empty bytes'\n\n# bytes with various values\nresult = []\nfor b in b'\\x00\\x01\\xff':\n    result.append(b)\nassert result == [0, 1, 255], 'iterate over bytes with special values'\n\n# === Range iteration (existing functionality) ===\nresult = []\nfor i in range(3):\n    result.append(i)\nassert result == [0, 1, 2], 'iterate over range'\n\n# range with step\nresult = []\nfor i in range(0, 6, 2):\n    result.append(i)\nassert result == [0, 2, 4], 'iterate over range with step'\n\n# === Nested iteration ===\nresult = []\nfor outer in [[1, 2], [3, 4]]:\n    for inner in outer:\n        result.append(inner)\nassert result == [1, 2, 3, 4], 'nested for loops'\n\n# iterate over string within list\nresult = []\nfor s in ['ab', 'cd']:\n    for c in s:\n        result.append(c)\nassert result == ['a', 'b', 'c', 'd'], 'nested string iteration'\n\n# === Using loop variable after loop ===\nfor x in [1, 2, 3]:\n    pass\nassert x == 3, 'loop variable persists after loop'\n\nfor y in 'abc':\n    pass\nassert y == 'c', 'string loop variable persists'\n\n# === List mutation during iteration ===\n# Python allows list mutation during iteration (unlike dict).\n# The iterator checks current length on each iteration.\n\n# appending during iteration - new items are seen\nresult = []\nlst = [1, 2, 3]\nfor x in lst:\n    result.append(x)\n    if x == 2:\n        lst.append(4)\nassert result == [1, 2, 3, 4], 'appending to list during iteration sees new items'\nassert lst == [1, 2, 3, 4], 'list was modified'\n\n# appending multiple items\nresult = []\nlst = [1]\nfor x in lst:\n    result.append(x)\n    if x < 5:\n        lst.append(x + 1)\nassert result == [1, 2, 3, 4, 5], 'can grow list dynamically during iteration'\n\n# === Modifying via copy pattern ===\noriginal = [1, 2, 3]\ncopy = list(original)\nfor x in copy:\n    if x == 2:\n        original.append(4)\nassert original == [1, 2, 3, 4], 'modifying list via copy pattern'\n\n# === Sum pattern ===\ntotal = 0\nfor n in [1, 2, 3, 4, 5]:\n    total = total + n\nassert total == 15, 'sum pattern with list'\n\n# === Early break simulation via flag ===\n# (break not implemented, using flag pattern)\nfound = False\nfor x in [1, 2, 3, 4, 5]:\n    if not found and x == 3:\n        found = True\nassert found == True, 'find pattern with flag'\n\n# === Accumulator patterns ===\n# count items\ncount = 0\nfor _ in ['a', 'b', 'c']:\n    count = count + 1\nassert count == 3, 'count items'\n\n# concatenate strings\nresult = ''\nfor s in ['a', 'b', 'c']:\n    result = result + s\nassert result == 'abc', 'concatenate strings'\n\n# === Dict key-value access pattern ===\nd = {'x': 10, 'y': 20}\ntotal = 0\nfor k in d:\n    total = total + d[k]\nassert total == 30, 'dict key-value access in loop'\n\n# === Dict mutation during iteration ===\n# Python allows modifying existing key values during iteration (no size change).\n# It also allows pop + add that keeps size the same (iterator sees new keys).\n\n# modifying existing values is allowed\nd = {'a': 1, 'b': 2, 'c': 3}\nfor k in d:\n    d[k] = d[k] * 10\nassert d == {'a': 10, 'b': 20, 'c': 30}, 'modify dict values during iteration'\n\n# pop + add keeping same size is allowed, iterator sees new keys\nd = {'a': 1, 'b': 2, 'c': 3}\nresult = []\nfor k in d:\n    result.append(k)\n    if k == 'a':\n        d.pop('b')\n        d['x'] = 4  # size unchanged\nassert result == ['a', 'c', 'x'], 'dict pop+add same size sees new keys'\nassert d == {'a': 1, 'c': 3, 'x': 4}, 'dict was modified correctly'\n"
  },
  {
    "path": "crates/monty/test_cases/iter__for_loop_unpacking.py",
    "content": "# === Basic for loop ===\nresult = []\nfor i in range(5):\n    result.append(i)\nassert result == [0, 1, 2, 3, 4], 'basic for loop'\n\n# === Tuple unpacking in for loop ===\npairs = [(1, 2), (3, 4), (5, 6)]\nsums = []\nfor a, b in pairs:\n    sums.append(a + b)\nassert sums == [3, 7, 11], 'for loop with pair unpacking'\n\n# === Triple unpacking ===\ntriples = [(1, 2, 3), (4, 5, 6)]\nproducts = []\nfor a, b, c in triples:\n    products.append(a * b * c)\nassert products == [6, 120], 'for loop with triple unpacking'\n\n# === Nested tuple unpacking ===\nnested = [((1, 2), 3), ((4, 5), 6)]\nresults = []\nfor (a, b), c in nested:\n    results.append(a + b + c)\nassert results == [6, 15], 'for loop with nested unpacking'\n\n# === Deep nested unpacking ===\ndeep = [((1, 2), (3, 4)), ((5, 6), (7, 8))]\nsums = []\nfor (a, b), (c, d) in deep:\n    sums.append(a + b + c + d)\nassert sums == [10, 26], 'for loop with deep nested unpacking'\n\n# === Mixed depth unpacking ===\nmixed = [(1, (2, 3)), (4, (5, 6))]\nresults = []\nfor a, (b, c) in mixed:\n    results.append(a + b + c)\nassert results == [6, 15], 'for loop with mixed depth unpacking'\n\n# === Unpacking with else clause ===\npairs = [(1, 2), (3, 4)]\ntotal = 0\nfor a, b in pairs:\n    total += a + b\nelse:\n    total += 100\nassert total == 110, 'for loop unpacking with else clause'\n\n# === Enumerate with unpacking ===\nitems = ['a', 'b', 'c']\nresult = []\nfor i, val in enumerate(items):\n    result.append((i, val))\nassert result == [(0, 'a'), (1, 'b'), (2, 'c')], 'enumerate with unpacking'\n\n# === Dict items unpacking ===\nd = {'x': 1, 'y': 2}\nkeys = []\nvals = []\nfor k, v in d.items():\n    keys.append(k)\n    vals.append(v)\nassert sorted(keys) == ['x', 'y'], 'dict items unpacking keys'\nassert sorted(vals) == [1, 2], 'dict items unpacking values'\n"
  },
  {
    "path": "crates/monty/test_cases/iter__generator_expr.py",
    "content": "# === Basic generator expression ===\nresult = list(x * 2 for x in range(5))\nassert result == [0, 2, 4, 6, 8], 'basic generator expression'\n\n# === With condition ===\nresult = list(x for x in range(10) if x % 2 == 0)\nassert result == [0, 2, 4, 6, 8], 'generator with condition'\n\n# === Nested generators ===\nresult = list(x + y for x in range(3) for y in range(2))\nassert result == [0, 1, 1, 2, 2, 3], 'nested generator'\n\n# === Generator in function call ===\nresult = sum(x for x in range(5))\nassert result == 10, 'generator in sum()'\n\n# === Generator with unpacking ===\npairs = [(1, 2), (3, 4)]\nresult = list(a + b for a, b in pairs)\nassert result == [3, 7], 'generator with unpacking'\n"
  },
  {
    "path": "crates/monty/test_cases/iter__generator_expr_type.py",
    "content": "# xfail=cpython\n# TODO: When proper generators are implemented, this test should be removed.\n# Currently generator expressions return lists in Monty, not generator objects.\n# This test verifies the temporary behavior until generators are properly implemented.\ngen_result = (x * 2 for x in range(5))\nassert type(gen_result) == list, 'generator expr currently returns list'\nassert gen_result == [0, 2, 4, 6, 8], 'generator expr value'\n"
  },
  {
    "path": "crates/monty/test_cases/iter__not_iterable.py",
    "content": "for x in 42:\n    pass\n# Raise=TypeError(\"'int' object is not iterable\")\n"
  },
  {
    "path": "crates/monty/test_cases/lambda__all.py",
    "content": "# === Basic lambda ===\n# no-arg lambda\nf = lambda: 42\nassert f() == 42, 'no-arg lambda'\n\n# single arg lambda\nf = lambda x: x + 1\nassert f(5) == 6, 'single arg lambda'\n\n# === Multiple arguments ===\nf = lambda x, y: x + y\nassert f(2, 3) == 5, 'multi-arg lambda'\n\nf = lambda x, y, z: x * y + z\nassert f(2, 3, 4) == 10, 'three-arg lambda'\n\n# === Default arguments ===\nf = lambda x, y=10: x + y\nassert f(5) == 15, 'lambda with default'\nassert f(5, 3) == 8, 'lambda override default'\n\nf = lambda x=1, y=2: x * y\nassert f() == 2, 'lambda all defaults'\nassert f(3) == 6, 'lambda override first default'\nassert f(3, 4) == 12, 'lambda override all defaults'\n\n# === Lambda as expression (immediate call) ===\nassert (lambda x: x * 2)(5) == 10, 'immediate call'\nassert (lambda: 'hello')() == 'hello', 'immediate call no args'\nassert (lambda x, y: x - y)(10, 3) == 7, 'immediate call multi args'\n\n# === Lambda in data structures ===\nfuncs = [lambda x: x + 1, lambda x: x * 2, lambda x: x**2]\nassert funcs[0](3) == 4, 'lambda in list - add'\nassert funcs[1](3) == 6, 'lambda in list - mul'\nassert funcs[2](3) == 9, 'lambda in list - pow'\n\n# === Lambda assigned and called later ===\nsquare = lambda x: x * x\ndouble = lambda x: x + x\nassert square(4) == 16, 'lambda assigned square'\nassert double(4) == 8, 'lambda assigned double'\n\n# === Lambda with operations ===\nf = lambda x: x > 5\nassert f(6) is True, 'lambda comparison gt'\nassert f(4) is False, 'lambda comparison not gt'\n\nf = lambda x: x if x > 0 else -x\nassert f(5) == 5, 'lambda ternary positive'\nassert f(-5) == 5, 'lambda ternary negative'\n\n\n# === Closures ===\ndef make_adder(n):\n    return lambda x: x + n\n\n\nadd5 = make_adder(5)\nadd10 = make_adder(10)\nassert add5(3) == 8, 'closure capture add5'\nassert add10(3) == 13, 'closure capture add10'\n\n\ndef make_multiplier(factor):\n    return lambda x: x * factor\n\n\ntimes3 = make_multiplier(3)\nassert times3(4) == 12, 'closure capture multiplier'\n\n# === Nested lambdas ===\nf = lambda x: lambda y: x + y\nadd_to_5 = f(5)\nassert add_to_5(3) == 8, 'nested lambda'\n\nf = lambda x: lambda y: lambda z: x + y + z\nassert f(1)(2)(3) == 6, 'triple nested lambda'\n\n# === Lambda in list comprehension ===\nsquared = [f(x) for x in [1, 2, 3, 4] for f in [lambda n: n * n]]\n# Note: this tests lambda in comprehension context, though due to late binding\n# all items use the same lambda\n\n# === Lambda returns another lambda ===\ncompose = lambda f: lambda g: lambda x: f(g(x))\ninc = lambda x: x + 1\ndouble = lambda x: x * 2\ninc_then_double = compose(double)(inc)\nassert inc_then_double(3) == 8, 'lambda composition'  # double(inc(3)) = double(4) = 8\n\n# === Lambda repr ===\nf = lambda: None\nr = repr(f)\nassert '<lambda>' in r, 'lambda repr contains <lambda>'\nassert 'function' in r, 'lambda repr contains function'\n\n# === Lambda with *args ===\nf = lambda *args: sum(args)\nassert f() == 0, 'lambda varargs empty'\nassert f(1) == 1, 'lambda varargs one'\nassert f(1, 2, 3) == 6, 'lambda varargs multiple'\n\n# === Lambda with keyword arguments ===\nf = lambda x, *, y: x + y\nassert f(1, y=2) == 3, 'lambda keyword only'\n\nf = lambda **kwargs: len(kwargs)\nassert f() == 0, 'lambda kwargs empty'\nassert f(a=1, b=2) == 2, 'lambda kwargs multiple'\n\n# === Mixed parameters ===\nf = lambda a, b=2, *args, c, d=4, **kwargs: (a, b, args, c, d, len(kwargs))\nresult = f(1, 2, 3, 4, c=10, d=20, e=30, f=40)\nassert result == (1, 2, (3, 4), 10, 20, 2), 'lambda mixed params'\n\n# === Unpacking in immediate lambda calls ===\nxs = [1, 2, 3]\nassert (lambda *a: a)(*xs) == (1, 2, 3), 'lambda with *args unpacking'\nassert (lambda **k: k)(**{'a': 1}) == {'a': 1}, 'lambda with **kwargs unpacking'\nassert (lambda *a, **k: (a, k))(1, 2, x=3) == ((1, 2), {'x': 3}), 'lambda mixed unpack'\n\n# === Lambda parameter shadowing ===\n# Inner lambda shadows outer variable - outer should not capture it\n\n\ndef make_shadowing_lambda():\n    x = 10\n    # inner lambda has param x, so outer lambda should NOT capture x from make_shadowing_lambda\n    return lambda: (lambda x: x + 1)\n\n\nouter_fn = make_shadowing_lambda()\ninner_fn = outer_fn()\nassert inner_fn(5) == 6, 'inner lambda takes x as param'\n\n\ndef test_inner_lambda_capture():\n    y = 5\n    # outer lambda binds y as param, inner lambda captures from outer lambda, not test_inner_lambda_capture\n    g = lambda y: (lambda: y)\n    return g(7)()\n\n\nassert test_inner_lambda_capture() == 7, 'inner lambda captures outer lambda param'\n"
  },
  {
    "path": "crates/monty/test_cases/list__extend_not_iterable.py",
    "content": "# Regression test: list.extend() with a non-iterable should still raise TypeError\n# with the correct message. This verifies that the list_extend opcode helper in\n# collections.rs (for [*expr] literals) and the list.extend() method in\n# types/list.rs are separate code paths that do not interfere with each other.\na = []\na.extend(1)\n# Raise=TypeError(\"'int' object is not iterable\")\n"
  },
  {
    "path": "crates/monty/test_cases/list__getitem_out_of_bounds.py",
    "content": "a = [1, 2, 3]\na[10]\n# Raise=IndexError('list index out of range')\n"
  },
  {
    "path": "crates/monty/test_cases/list__index_not_found.py",
    "content": "[1, 2, 3].index(4)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__index_not_found.py\", line 1, in <module>\n    [1, 2, 3].index(4)\n    ~~~~~~~~~~~~~~~~~~\nValueError: list.index(x): x not in list\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__index_start_gt_end.py",
    "content": "# Test that list.index with start > end doesn't panic but raises ValueError\n[1, 2, 3].index(1, 5, 2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__index_start_gt_end.py\", line 2, in <module>\n    [1, 2, 3].index(1, 5, 2)\n    ~~~~~~~~~~~~~~~~~~~~~~~~\nValueError: list.index(x): x not in list\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__ops.py",
    "content": "# === List concatenation (+) ===\nassert [1, 2] + [3, 4] == [1, 2, 3, 4], 'basic concat'\nassert [] + [1, 2] == [1, 2], 'empty left concat'\nassert [1, 2] + [] == [1, 2], 'empty right concat'\nassert [] + [] == [], 'empty both concat'\nassert [1] + [2] + [3] + [4] == [1, 2, 3, 4], 'multiple concat'\nassert [[1]] + [[2]] == [[1], [2]], 'nested concat'\n\n# === Augmented assignment (+=) ===\nlst = [1, 2]\nlst += [3, 4]\nassert lst == [1, 2, 3, 4], 'basic iadd'\n\nlst = [1]\nalias = lst\nlst += [2]\nassert lst is alias, 'list += preserves identity'\nassert alias == [1, 2], 'list += mutates through aliases'\n\nlst = [1, 2, 3]\nindex = 1\nlst[index] += 5\nassert lst == [1, 7, 3], 'subscript += updates the selected list item'\n\ntry:\n    lst = [1]\n    lst[5] += 1\n    assert False, 'subscript += past the end of a list should raise IndexError'\nexcept IndexError as e:\n    assert e.args == ('list index out of range',), 'subscript += list index error matches normal setitem'\n\nlst = [1]\nlst += []\nassert lst == [1], 'iadd empty'\n\nlst = [1]\nlst += [2]\nlst += [3]\nassert lst == [1, 2, 3], 'multiple iadd'\n\nlst = [1, 2]\nlst += lst\nassert lst == [1, 2, 1, 2], 'iadd self'\n\n# === List length ===\nassert len([]) == 0, 'len empty'\nassert len([1, 2, 3]) == 3, 'len basic'\n\nlst = [1]\nlst.append(2)\nassert len(lst) == 2, 'len after append'\n\n# === List indexing ===\na = []\na.append('value')\nassert a[0] == 'value', 'getitem basic'\n\na = [1, 2, 3]\nassert a[0 - 1] == 3, 'getitem negative index'\nassert a[-1] == 3, 'getitem -1'\nassert a[-2] == 2, 'getitem -2'\n\n# === List repr/str ===\nassert repr([]) == '[]', 'empty list repr'\nassert str([]) == '[]', 'empty list str'\n\nassert repr([1, 2, 3]) == '[1, 2, 3]', 'list repr'\nassert str([1, 2, 3]) == '[1, 2, 3]', 'list str'\n\n# === List repetition (*) ===\nassert [1, 2] * 3 == [1, 2, 1, 2, 1, 2], 'list mult int'\nassert 3 * [1, 2] == [1, 2, 1, 2, 1, 2], 'int mult list'\nassert [1] * 0 == [], 'list mult zero'\nassert [1] * -1 == [], 'list mult negative'\nassert [] * 5 == [], 'empty list mult'\nassert [1, 2] * 1 == [1, 2], 'list mult one'\nassert [[1]] * 2 == [[1], [1]], 'nested list mult'\n\n# === List repetition augmented assignment (*=) ===\nlst = [1, 2]\nlst *= 2\nassert lst == [1, 2, 1, 2], 'list imult'\n\nlst = [1]\nlst *= 0\nassert lst == [], 'list imult zero'\n\n# === list() constructor ===\nassert list() == [], 'list() empty'\nassert list([1, 2, 3]) == [1, 2, 3], 'list from list'\nassert list((1, 2, 3)) == [1, 2, 3], 'list from tuple'\nassert list(range(3)) == [0, 1, 2], 'list from range'\nassert list('abc') == ['a', 'b', 'c'], 'list from string'\nassert list(b'abc') == [97, 98, 99], 'list from bytes'\nassert list({'a': 1, 'b': 2}) == ['a', 'b'], 'list from dict yields keys'\n\n# non-ASCII strings (multi-byte UTF-8)\nassert list('héllo') == ['h', 'é', 'l', 'l', 'o'], 'list from string with accented char'\nassert list('日本') == ['日', '本'], 'list from string with CJK chars'\nassert list('a🎉b') == ['a', '🎉', 'b'], 'list from string with emoji'\n\n# === list.append() ===\nlst = []\nlst.append(1)\nassert lst == [1], 'append to empty'\nlst.append(2)\nassert lst == [1, 2], 'append to non-empty'\nlst.append(lst)  # append self creates cycle\nassert len(lst) == 3, 'append self increases length'\n\n# === list.insert() ===\n# Basic insert at various positions\nlst = [1, 2, 3]\nlst.insert(0, 'a')\nassert lst == ['a', 1, 2, 3], 'insert at beginning'\n\nlst = [1, 2, 3]\nlst.insert(1, 'a')\nassert lst == [1, 'a', 2, 3], 'insert in middle'\n\nlst = [1, 2, 3]\nlst.insert(3, 'a')\nassert lst == [1, 2, 3, 'a'], 'insert at end'\n\n# Insert beyond length appends\nlst = [1, 2, 3]\nlst.insert(100, 'a')\nassert lst == [1, 2, 3, 'a'], 'insert beyond length appends'\n\n# Insert with negative index\nlst = [1, 2, 3]\nlst.insert(-1, 'a')\nassert lst == [1, 2, 'a', 3], 'insert at -1 (before last)'\n\nlst = [1, 2, 3]\nlst.insert(-2, 'a')\nassert lst == [1, 'a', 2, 3], 'insert at -2'\n\nlst = [1, 2, 3]\nlst.insert(-100, 'a')\nassert lst == ['a', 1, 2, 3], 'insert very negative clamps to 0'\n\n# === list.pop() ===\nlst = [1, 2, 3]\nassert lst.pop() == 3, 'pop without argument returns last'\nassert lst == [1, 2], 'pop removes last element'\n\nlst = [1, 2, 3]\nassert lst.pop(0) == 1, 'pop(0) returns first'\nassert lst == [2, 3], 'pop(0) removes first element'\n\nlst = [1, 2, 3]\nassert lst.pop(1) == 2, 'pop(1) returns middle'\nassert lst == [1, 3], 'pop(1) removes middle element'\n\nlst = [1, 2, 3]\nassert lst.pop(-1) == 3, 'pop(-1) returns last'\nassert lst == [1, 2], 'pop(-1) removes last element'\n\nlst = [1, 2, 3]\nassert lst.pop(-2) == 2, 'pop(-2) returns second to last'\nassert lst == [1, 3], 'pop(-2) removes second to last element'\n\n# === list.remove() ===\nlst = [1, 2, 3, 2]\nlst.remove(2)\nassert lst == [1, 3, 2], 'remove removes first occurrence'\n\nlst = ['a', 'b', 'c']\nlst.remove('b')\nassert lst == ['a', 'c'], 'remove string element'\n\n# === list.clear() ===\nlst = [1, 2, 3]\nlst.clear()\nassert lst == [], 'clear empties the list'\n\nlst = []\nlst.clear()\nassert lst == [], 'clear on empty list is no-op'\n\n# === list.copy() ===\nlst = [1, 2, 3]\ncopy = lst.copy()\nassert copy == [1, 2, 3], 'copy creates equal list'\nassert copy is not lst, 'copy creates new list object'\nlst.append(4)\nassert copy == [1, 2, 3], 'copy is independent'\n\n# === list.extend() ===\nlst = [1, 2]\nlst.extend([3, 4])\nassert lst == [1, 2, 3, 4], 'extend with list'\n\nlst = [1]\nlst.extend((2, 3))\nassert lst == [1, 2, 3], 'extend with tuple'\n\nlst = [1]\nlst.extend(range(2, 5))\nassert lst == [1, 2, 3, 4], 'extend with range'\n\nlst = [1]\nlst.extend('ab')\nassert lst == [1, 'a', 'b'], 'extend with string'\n\nlst = []\nlst.extend([])\nassert lst == [], 'extend empty with empty'\n\n# === list.index() ===\nlst = [1, 2, 3, 2]\nassert lst.index(2) == 1, 'index finds first occurrence'\nassert lst.index(3) == 2, 'index finds element'\nassert lst.index(2, 2) == 3, 'index with start'\nassert lst.index(2, 1, 4) == 1, 'index with start and end'\n\n# === list.count() ===\nlst = [1, 2, 2, 3, 2]\nassert lst.count(2) == 3, 'count multiple occurrences'\nassert lst.count(1) == 1, 'count single occurrence'\nassert lst.count(4) == 0, 'count zero occurrences'\nassert [].count(1) == 0, 'count on empty list'\n\n# === list.reverse() ===\nlst = [1, 2, 3]\nlst.reverse()\nassert lst == [3, 2, 1], 'reverse modifies in place'\n\nlst = [1]\nlst.reverse()\nassert lst == [1], 'reverse single element'\n\nlst = []\nlst.reverse()\nassert lst == [], 'reverse empty list'\n\n# === list.sort() ===\nlst = [3, 1, 2]\nlst.sort()\nassert lst == [1, 2, 3], 'sort integers'\n\nlst = ['b', 'c', 'a']\nlst.sort()\nassert lst == ['a', 'b', 'c'], 'sort strings'\n\nlst = [3, 1, 2]\nlst.sort(reverse=True)\nassert lst == [3, 2, 1], 'sort with reverse=True'\n\nlst = []\nlst.sort()\nassert lst == [], 'sort empty list'\n\nlst = [1]\nlst.sort()\nassert lst == [1], 'sort single element'\n\n# === list.sort(key=...) ===\nlst = ['banana', 'apple', 'cherry']\nlst.sort(key=len)\nassert lst == ['apple', 'banana', 'cherry'], 'sort by len'\n\nlst = [[1, 2, 3], [4], [5, 6]]\nlst.sort(key=len)\nassert lst == [[4], [5, 6], [1, 2, 3]], 'sort nested lists by len'\n\nlst = [[1, 2, 3], [4], [5, 6]]\nlst.sort(key=len, reverse=True)\nassert lst == [[1, 2, 3], [5, 6], [4]], 'sort by len reverse'\n\nlst = [-3, 1, -2, 4]\nlst.sort(key=abs)\nassert lst == [1, -2, -3, 4], 'sort by abs'\n\n# key=None is same as no key\nlst = [3, 1, 2]\nlst.sort(key=None)\nassert lst == [1, 2, 3], 'sort with key=None'\n\nlst = [3, 1, 2]\nlst.sort(key=None, reverse=True)\nassert lst == [3, 2, 1], 'sort with key=None reverse'\n\n# Empty list with key\nlst = []\nlst.sort(key=len)\nassert lst == [], 'sort empty list with key'\n\n# key=int for string-to-int conversion\nlst = ['-3', '1', '-2', '4']\nlst.sort(key=int)\nassert lst == ['-3', '-2', '1', '4'], 'sort strings by int value'\n\nlst = ['10', '2', '1', '100']\nlst.sort(key=int)\nassert lst == ['1', '2', '10', '100'], 'sort numeric strings by int value'\n\nlst = ['10', '2', '1', '100']\nlst.sort(key=int, reverse=True)\nassert lst == ['100', '10', '2', '1'], 'sort numeric strings by int reverse'\n\n# user-defined key function\n\n\ndef last_char(s):\n    return s[-1]\n\n\nlst = ['cherry', 'banana', 'apple']\nlst.sort(key=last_char)\nassert lst == ['banana', 'apple', 'cherry'], 'sort by last char'\n\n\n# key function might raise exception\nlst = ['']\ntry:\n    lst.sort(key=last_char)\nexcept IndexError:\n    pass  # expected since last_char('') raises IndexError\n\n\n# === List assignment (setitem) ===\n# Basic assignment\nlst = [1, 2, 3]\nlst[0] = 10\nassert lst == [10, 2, 3], 'setitem at index 0'\n\nlst = [1, 2, 3]\nlst[1] = 20\nassert lst == [1, 20, 3], 'setitem at index 1'\n\nlst = [1, 2, 3]\nlst[2] = 30\nassert lst == [1, 2, 30], 'setitem at last index'\n\n# Negative index assignment\nlst = [1, 2, 3]\nlst[-1] = 100\nassert lst == [1, 2, 100], 'setitem at -1'\n\nlst = [1, 2, 3]\nlst[-2] = 200\nassert lst == [1, 200, 3], 'setitem at -2'\n\nlst = [1, 2, 3]\nlst[-3] = 300\nassert lst == [300, 2, 3], 'setitem at -3'\n\n# Assigning different types\nlst = [1, 2, 3]\nlst[0] = 'hello'\nassert lst == ['hello', 2, 3], 'setitem string value'\n\nlst = [1, 2, 3]\nlst[1] = [4, 5]\nassert lst == [1, [4, 5], 3], 'setitem list value'\n\nlst = [1, 2, 3]\nlst[0] = None\nassert lst == [None, 2, 3], 'setitem None value'\n\n# Multiple assignments\nlst = [0, 0, 0]\nlst[0] = 1\nlst[1] = 2\nlst[2] = 3\nassert lst == [1, 2, 3], 'multiple setitem'\n\n# Assignment preserves other elements\nlst = ['a', 'b', 'c', 'd']\nlst[1] = 'B'\nassert lst[0] == 'a', 'setitem preserves element 0'\nassert lst[1] == 'B', 'setitem changes element 1'\nassert lst[2] == 'c', 'setitem preserves element 2'\nassert lst[3] == 'd', 'setitem preserves element 3'\n\n# === Bool indices ===\n# Python allows True/False as indices (True=1, False=0)\nlst = ['a', 'b', 'c']\nassert lst[False] == 'a', 'getitem with False'\nassert lst[True] == 'b', 'getitem with True'\n\nlst = ['x', 'y', 'z']\nlst[False] = 'X'\nassert lst == ['X', 'y', 'z'], 'setitem with False'\n\nlst = ['x', 'y', 'z']\nlst[True] = 'Y'\nassert lst == ['x', 'Y', 'z'], 'setitem with True'\n\n# === Nested list equality ===\n# same-length lists with matching nested elements\nassert [[1, 2], [3, 4]] == [[1, 2], [3, 4]], 'nested list eq'\n# same-length but different nested elements (exercises py_eq early return)\nassert [[1, 2], [3, 4]] != [[1, 2], [3, 5]], 'nested list ne same length'\nassert [[]] != [[1]], 'nested empty vs non-empty'\n# deeper nesting\nassert [[[1]]] == [[[1]]], 'deep nested list eq'\nassert [[[1]]] != [[[2]]], 'deep nested list ne'\n# mixed nesting depths\nassert [[1], 2] == [[1], 2], 'mixed nesting eq'\nassert [[1], 2] != [[1], 3], 'mixed nesting ne'\n\n# === Nested list repr ===\nassert repr([[1, 2], [3, 4]]) == '[[1, 2], [3, 4]]', 'nested list repr'\nassert repr([[]]) == '[[]]', 'list containing empty list repr'\nassert repr([[1], [2, 3]]) == '[[1], [2, 3]]', 'nested varied len repr'\n\n# === list.remove() with nested elements ===\nx = [1, 2]\nlst = [x, [3, 4], x]\nlst.remove([1, 2])\nassert lst == [[3, 4], [1, 2]], 'remove nested list element'\n\nlst = [1, [2, 3], 4]\nlst.remove([2, 3])\nassert lst == [1, 4], 'remove nested from mixed'\n\n# === list.index() with nested elements ===\nlst = [[3], [1, 2], [4]]\nassert lst.index([1, 2]) == 1, 'index with nested list'\n\nlst = [[1], [2], [1]]\nassert lst.index([1]) == 0, 'index nested finds first'\n\n# === list.count() with nested elements ===\nlst = [[1, 2], [3], [1, 2], 4, [1, 2]]\nassert lst.count([1, 2]) == 3, 'count nested list elements'\nassert lst.count([3]) == 1, 'count single nested occurrence'\nassert lst.count([99]) == 0, 'count nested not found'\nassert [].count([1]) == 0, 'count on empty list'\n\n# === Nested list containment ===\nassert [1, 2] in [[1, 2], [3, 4]], 'nested list in'\nassert [5, 6] not in [[1, 2], [3, 4]], 'nested list not in'\nassert [] in [[], [1]], 'empty list in list of lists'\n\n# === List unpacking (PEP 448) ===\na = [1, 2]\nb = [3, 4]\nassert [*a] == [1, 2], 'single list unpack'\nassert [*a, *b] == [1, 2, 3, 4], 'double list unpack'\nassert [0, *a, 5] == [0, 1, 2, 5], 'mixed list unpack'\nassert [*[]] == [], 'unpack empty list'\nassert [*(1, 2)] == [1, 2], 'unpack tuple into list'\nassert [*'abc'] == ['a', 'b', 'c'], 'unpack string into list'\nassert [*{'x': 1, 'y': 2}] == ['x', 'y'], 'unpack dict keys into list'\n# Heap-allocated set: covers the HeapData::Set arm in list_extend\nassert sorted([*{1, 2, 3}]) == [1, 2, 3], 'unpack set into list'\n# Heap-allocated Str (result of concat, not interned): covers HeapData::Str in list_extend\nhs = 'hel' + 'lo'\nassert [*hs] == ['h', 'e', 'l', 'l', 'o'], 'unpack heap string into list'\n\n\n# Non-iterable heap-allocated Ref (closure) hits the inner `_` arm in list_extend.\n# A plain top-level function is Value::DefFunction (not a Ref), so a closure is\n# required to reach the Value::Ref(_) branch (HeapData that is not List/Tuple/Set/Dict/Str).\ndef _make_list_unpack_closure():\n    _sentinel = 1\n\n    def _inner():\n        return _sentinel\n\n    return _inner\n\n\n_list_unpack_closure = _make_list_unpack_closure()\ntry:\n    _x = [*_list_unpack_closure]\n    assert False, 'expected TypeError for non-iterable heap closure in list unpack'\nexcept TypeError:\n    pass\n"
  },
  {
    "path": "crates/monty/test_cases/list__pop_empty.py",
    "content": "[].pop()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__pop_empty.py\", line 1, in <module>\n    [].pop()\n    ~~~~~~~~\nIndexError: pop from empty list\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__pop_out_of_range.py",
    "content": "[1, 2, 3].pop(10)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__pop_out_of_range.py\", line 1, in <module>\n    [1, 2, 3].pop(10)\n    ~~~~~~~~~~~~~~~~~\nIndexError: pop index out of range\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__pop_type_error.py",
    "content": "[].pop('not an int')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__pop_type_error.py\", line 1, in <module>\n    [].pop('not an int')\n    ~~~~~~~~~~~~~~~~~~~~\nTypeError: 'str' object cannot be interpreted as an integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__remove_not_found.py",
    "content": "[1, 2, 3].remove(4)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__remove_not_found.py\", line 1, in <module>\n    [1, 2, 3].remove(4)\n    ~~~~~~~~~~~~~~~~~~~\nValueError: list.remove(x): x not in list\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__setitem_dict_index.py",
    "content": "# Test using a dict as a list setitem index (should raise TypeError)\n# This covers the code path where a non-LongInt Ref type is used as an index\nlst = [1, 2, 3]\nd = {'key': 'value'}\nlst[d] = 42\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__setitem_dict_index.py\", line 5, in <module>\n    lst[d] = 42\n    ~~~~~~\nTypeError: list indices must be integers or slices, not dict\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__setitem_huge_int_index.py",
    "content": "# Test using a huge LongInt as a list setitem index (should raise IndexError)\n# This covers the code path where a LongInt exceeds i64 range\nlst = [1, 2, 3]\nhuge = 2**100\nlst[huge] = 42\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__setitem_huge_int_index.py\", line 5, in <module>\n    lst[huge] = 42\n    ~~~~~~~~~\nIndexError: cannot fit 'int' into an index-sized integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__setitem_index_error.py",
    "content": "lst = [1, 2, 3]\nlst[10] = 'value'\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__setitem_index_error.py\", line 2, in <module>\n    lst[10] = 'value'\n    ~~~~~~~\nIndexError: list assignment index out of range\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__setitem_type_error.py",
    "content": "lst = [1, 2, 3]\nlst['key'] = 'value'\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"list__setitem_type_error.py\", line 2, in <module>\n    lst['key'] = 'value'\n    ~~~~~~~~~~\nTypeError: list indices must be integers or slices, not str\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/list__unpack_type_error.py",
    "content": "[*42]\n# Raise=TypeError('Value after * must be an iterable, not int')\n"
  },
  {
    "path": "crates/monty/test_cases/longint__index_error.py",
    "content": "big = 10**100\n[1, 2, 3][big]\n# Raise=IndexError(\"cannot fit 'int' into an index-sized integer\")\n"
  },
  {
    "path": "crates/monty/test_cases/longint__repeat_error.py",
    "content": "big = 10**100\n'abc' * big\n# Raise=OverflowError(\"cannot fit 'int' into an index-sized integer\")\n"
  },
  {
    "path": "crates/monty/test_cases/loop__break_continue.py",
    "content": "# === Basic break ===\nresult = []\nfor x in [1, 2, 3, 4, 5]:\n    if x == 3:\n        break\n    result.append(x)\nassert result == [1, 2], 'break exits loop early'\n\n# === Break skips else ===\nflag = 0\nfor x in [1, 2, 3]:\n    if x == 2:\n        break\nelse:\n    flag = 1\nassert flag == 0, 'break skips else clause'\n\n# === No break runs else ===\nflag = 0\nfor x in [1, 2, 3]:\n    pass\nelse:\n    flag = 1\nassert flag == 1, 'completing loop runs else clause'\n\n# === Basic continue ===\nresult = []\nfor x in [1, 2, 3, 4, 5]:\n    if x % 2 == 0:\n        continue\n    result.append(x)\nassert result == [1, 3, 5], 'continue skips iteration'\n\n# === Continue with else ===\nflag = 0\nfor x in [1, 2, 3]:\n    if x == 2:\n        continue\nelse:\n    flag = 1\nassert flag == 1, 'continue does not skip else clause'\n\n# === Nested loops - break inner ===\nresult = []\nfor i in [1, 2, 3]:\n    for j in ['a', 'b', 'c']:\n        if j == 'b':\n            break\n        result.append((i, j))\nassert result == [(1, 'a'), (2, 'a'), (3, 'a')], 'break only affects inner loop'\n\n# === Nested loops - continue inner ===\nresult = []\nfor i in [1, 2]:\n    for j in ['a', 'b', 'c']:\n        if j == 'b':\n            continue\n        result.append((i, j))\nassert result == [(1, 'a'), (1, 'c'), (2, 'a'), (2, 'c')], 'continue only affects inner loop'\n\n# === Break in nested with else on inner ===\nresult = []\nfor i in [1, 2]:\n    for j in [10, 20, 30]:\n        if j == 20:\n            break\n        result.append(j)\n    else:\n        result.append('inner-else')\nassert result == [10, 10], 'break skips inner else'\n\n# === No break in inner runs inner else ===\nresult = []\nfor i in [1, 2]:\n    for j in [10, 20]:\n        result.append(j)\n    else:\n        result.append('inner-else')\nassert result == [10, 20, 'inner-else', 10, 20, 'inner-else'], 'no break runs inner else'\n\n# === Continue does not affect else ===\nresult = []\nfor x in [1, 2, 3]:\n    if x == 2:\n        continue\n    result.append(x)\nelse:\n    result.append('else')\nassert result == [1, 3, 'else'], 'continue does not prevent else'\n\n# === Empty loop with else ===\nflag = 0\nfor x in []:\n    flag = 1\nelse:\n    flag = 2\nassert flag == 2, 'empty loop runs else'\n\n# === Break on first iteration ===\nresult = []\nfor x in [1, 2, 3]:\n    result.append('before')\n    break\n    result.append('after')  # unreachable\nassert result == ['before'], 'break on first iteration'\n\n\n# === Double break (unreachable second break) ===\ndef double_break(value):\n    for i in range(0, 1):\n        break\n        break\n    return value\n\n\nassert double_break('hello') == 'hello', 'double break returns value correctly'\nassert double_break(42) == 42, 'double break works with int'\n\n\n# === Two breaks in different branches (both reachable) ===\ndef two_breaks(items):\n    result = []\n    for x in items:\n        if x < 0:\n            result.append('negative')\n            break\n        if x > 100:\n            result.append('too big')\n            break\n        result.append(x)\n    return result\n\n\nassert two_breaks([1, 2, 3]) == [1, 2, 3], 'no break taken'\nassert two_breaks([1, -1, 3]) == [1, 'negative'], 'first break taken'\nassert two_breaks([1, 200, 3]) == [1, 'too big'], 'second break taken'\nassert two_breaks([-5]) == ['negative'], 'negative on first item'\nassert two_breaks([999]) == ['too big'], 'too big on first item'\n\n\n# === Double continue (unreachable second continue) ===\ndef double_continue(items):\n    out = []\n    for x in items:\n        out.append(x)\n        continue\n        continue\n    return out\n\n\nassert double_continue([1, 2, 3]) == [1, 2, 3], 'double continue keeps normal loop output'\nassert double_continue([]) == [], 'double continue handles empty input'\n\n# === Continue on every iteration ===\nresult = []\nfor x in [1, 2, 3]:\n    result.append(x)\n    continue\n    result.append('after')  # unreachable\nassert result == [1, 2, 3], 'continue on every iteration'\n"
  },
  {
    "path": "crates/monty/test_cases/loop__break_finally.py",
    "content": "# === Break in try/finally must run finally ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        result.append('before')\n        break\n        result.append('after')  # unreachable\n    finally:\n        result.append('finally')\nassert result == ['before', 'finally'], f'break in try/finally should run finally: {result}'\n\n# === Break in nested try/finally runs both finally blocks ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        try:\n            result.append('inner-try')\n            break\n        finally:\n            result.append('inner-finally')\n    finally:\n        result.append('outer-finally')\nassert result == ['inner-try', 'inner-finally', 'outer-finally'], f'nested finally blocks: {result}'\n\n# === Break in try/except/finally runs finally ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        result.append('try')\n        break\n    except ValueError:\n        result.append('except')\n    finally:\n        result.append('finally')\nassert result == ['try', 'finally'], f'break in try/except/finally: {result}'\n\n# === Break inside except handler with finally ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        raise ValueError('test')\n    except ValueError:\n        result.append('except')\n        break\n    finally:\n        result.append('finally')\nassert result == ['except', 'finally'], f'break in except with finally: {result}'\n\n# === Break does not run finally if not in try ===\nresult = []\nfor x in [1, 2, 3]:\n    result.append('body')\n    break\nassert result == ['body'], f'break without finally: {result}'\n\n# === Break with multiple loops and finally ===\nresult = []\nfor i in [1, 2]:\n    try:\n        for j in [10, 20, 30]:\n            if j == 20:\n                break  # This break should not trigger outer finally\n            result.append(j)\n        result.append('after-inner')\n    finally:\n        result.append('outer-finally')\nassert result == [10, 'after-inner', 'outer-finally', 10, 'after-inner', 'outer-finally'], (\n    f'inner break with outer finally: {result}'\n)\n"
  },
  {
    "path": "crates/monty/test_cases/loop__break_in_function_error.py",
    "content": "def foo():\n    break\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"loop__break_in_function_error.py\", line 2\n    break\n    ~~~~~\nSyntaxError: 'break' outside loop\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/loop__break_in_if_error.py",
    "content": "x = True\nif x:\n    break\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"loop__break_in_if_error.py\", line 3\n    break\n    ~~~~~\nSyntaxError: 'break' outside loop\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/loop__break_nested_except_clears.py",
    "content": "# When breaking from nested except handlers, ALL exception states must be cleared.\n# After the loop completes via break, execution should continue normally.\n\n# Test 1: break from depth 2 should reach code after loop\ndef test_break():\n    for i in range(1):\n        try:\n            raise ValueError('outer')\n        except:\n            try:\n                raise TypeError('inner')\n            except:\n                break  # Should clear BOTH exceptions\n    return 'ok'\n\n\nassert test_break() == 'ok', 'break from nested except should reach return'\n\n\n# Test 2: break from depth 3 should also work\ndef test_break_depth3():\n    for i in range(1):\n        try:\n            raise ValueError('level1')\n        except:\n            try:\n                raise TypeError('level2')\n            except:\n                try:\n                    raise RuntimeError('level3')\n                except:\n                    break  # Should clear ALL THREE exceptions\n    return 'deep'\n\n\nassert test_break_depth3() == 'deep', 'break from 3-deep except should reach return'\n\n\n# Test 3: verify exception stack is empty after break\ndef test_empty_stack():\n    result = []\n    for i in range(1):\n        try:\n            raise ValueError('outer')\n        except:\n            try:\n                raise TypeError('inner')\n            except:\n                result.append('breaking')\n                break\n    result.append('after')\n    return result\n\n\nassert test_empty_stack() == ['breaking', 'after'], 'should execute code after break'\n"
  },
  {
    "path": "crates/monty/test_cases/loop__break_outside_error.py",
    "content": "break\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"loop__break_outside_error.py\", line 1\n    break\n    ~~~~~\nSyntaxError: 'break' outside loop\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/loop__continue_finally.py",
    "content": "# === Continue in try/finally must run finally ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        result.append(x)\n        if x == 2:\n            continue\n        result.append('after-continue')\n    finally:\n        result.append('finally')\nassert result == [1, 'after-continue', 'finally', 2, 'finally', 3, 'after-continue', 'finally'], (\n    f'continue in try/finally should run finally: {result}'\n)\n\n# === Continue in nested try/finally runs both finally blocks ===\nresult = []\nfor x in [1, 2]:\n    try:\n        try:\n            result.append(x)\n            continue\n        finally:\n            result.append('inner-finally')\n    finally:\n        result.append('outer-finally')\nassert result == [1, 'inner-finally', 'outer-finally', 2, 'inner-finally', 'outer-finally'], (\n    f'nested finally with continue: {result}'\n)\n\n# === Continue in try/except/finally runs finally ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        result.append(x)\n        if x == 2:\n            continue\n    except ValueError:\n        result.append('except')\n    finally:\n        result.append('finally')\nassert result == [1, 'finally', 2, 'finally', 3, 'finally'], f'continue in try/except/finally: {result}'\n\n# === Continue inside except handler with finally ===\nresult = []\nfor x in [1, 2, 3]:\n    try:\n        if x == 2:\n            raise ValueError('test')\n        result.append(x)\n    except ValueError:\n        result.append('except')\n        continue\n    finally:\n        result.append('finally')\n    result.append('after')\nassert result == [1, 'finally', 'after', 'except', 'finally', 3, 'finally', 'after'], (\n    f'continue in except with finally: {result}'\n)\n\n# === Continue does not run finally if not in try ===\nresult = []\nfor x in [1, 2, 3]:\n    result.append(x)\n    continue\n    result.append('unreachable')\nassert result == [1, 2, 3], f'continue without finally: {result}'\n\n# === Continue with multiple loops and finally ===\nresult = []\nfor i in [1, 2]:\n    try:\n        for j in [10, 20, 30]:\n            if j == 20:\n                continue  # This continue should not trigger outer finally\n            result.append(j)\n        result.append('after-inner')\n    finally:\n        result.append('outer-finally')\nassert result == [10, 30, 'after-inner', 'outer-finally', 10, 30, 'after-inner', 'outer-finally'], (\n    f'inner continue with outer finally: {result}'\n)\n"
  },
  {
    "path": "crates/monty/test_cases/loop__continue_in_function_error.py",
    "content": "def foo():\n    continue\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"loop__continue_in_function_error.py\", line 2\n    continue\n    ~~~~~~~~\nSyntaxError: 'continue' not properly in loop\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/loop__continue_in_if_error.py",
    "content": "x = True\nif x:\n    continue\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"loop__continue_in_if_error.py\", line 3\n    continue\n    ~~~~~~~~\nSyntaxError: 'continue' not properly in loop\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/loop__continue_nested_except_clears.py",
    "content": "# When continuing from nested except handlers, ALL exception states must be cleared.\n# After the loop completes its iterations, execution should continue normally.\n\n# Test 1: continue from depth 2 should process all iterations\ndef test_continue():\n    results = []\n    for i in range(3):\n        try:\n            raise ValueError('outer')\n        except:\n            try:\n                raise TypeError('inner')\n            except:\n                results.append(i)\n                continue  # Should clear BOTH exceptions\n    return results\n\n\nassert test_continue() == [0, 1, 2], 'continue from nested except should process all iterations'\n\n\n# Test 2: continue from depth 3 should also work\ndef test_continue_depth3():\n    results = []\n    for i in range(2):\n        try:\n            raise ValueError('level1')\n        except:\n            try:\n                raise TypeError('level2')\n            except:\n                try:\n                    raise RuntimeError('level3')\n                except:\n                    results.append(i)\n                    continue  # Should clear ALL THREE exceptions\n    return results\n\n\nassert test_continue_depth3() == [0, 1], 'continue from 3-deep except should work'\n\n\n# Test 3: continue runs else clause since loop completes normally\ndef test_continue_with_else():\n    results = []\n    for i in range(2):\n        try:\n            raise ValueError('outer')\n        except:\n            try:\n                raise TypeError('inner')\n            except:\n                results.append(i)\n                continue\n    else:\n        results.append('else')\n    return results\n\n\nassert test_continue_with_else() == [0, 1, 'else'], 'continue should allow else to run'\n"
  },
  {
    "path": "crates/monty/test_cases/loop__continue_outside_error.py",
    "content": "continue\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"loop__continue_outside_error.py\", line 1\n    continue\n    ~~~~~~~~\nSyntaxError: 'continue' not properly in loop\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__acos_domain_error.py",
    "content": "import math\n\nmath.acos(2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__acos_domain_error.py\", line 3, in <module>\n    math.acos(2)\n    ~~~~~~~~~~~~\nValueError: expected a number in range from -1 up to 1, got 2.0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__acosh_domain_error.py",
    "content": "import math\n\nmath.acosh(0.5)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__acosh_domain_error.py\", line 3, in <module>\n    math.acosh(0.5)\n    ~~~~~~~~~~~~~~~\nValueError: expected argument value not less than 1, got 0.5\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__asin_domain_error.py",
    "content": "import math\n\nmath.asin(2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__asin_domain_error.py\", line 3, in <module>\n    math.asin(2)\n    ~~~~~~~~~~~~\nValueError: expected a number in range from -1 up to 1, got 2.0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__atanh_domain_error.py",
    "content": "import math\n\nmath.atanh(1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__atanh_domain_error.py\", line 3, in <module>\n    math.atanh(1)\n    ~~~~~~~~~~~~~\nValueError: expected a number between -1 and 1, got 1.0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__cos_inf_error.py",
    "content": "import math\n\nmath.cos(math.inf)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__cos_inf_error.py\", line 3, in <module>\n    math.cos(math.inf)\n    ~~~~~~~~~~~~~~~~~~\nValueError: expected a finite input, got inf\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__cosh_overflow_error.py",
    "content": "import math\n\nmath.cosh(1000)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__cosh_overflow_error.py\", line 3, in <module>\n    math.cosh(1000)\n    ~~~~~~~~~~~~~~~\nOverflowError: math range error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__exp_overflow_error.py",
    "content": "import math\n\nmath.exp(1000)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__exp_overflow_error.py\", line 3, in <module>\n    math.exp(1000)\n    ~~~~~~~~~~~~~~\nOverflowError: math range error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__factorial_float_error.py",
    "content": "import math\n\nmath.factorial(1.5)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__factorial_float_error.py\", line 3, in <module>\n    math.factorial(1.5)\n    ~~~~~~~~~~~~~~~~~~~\nTypeError: 'float' object cannot be interpreted as an integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__factorial_negative_error.py",
    "content": "import math\n\nmath.factorial(-1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__factorial_negative_error.py\", line 3, in <module>\n    math.factorial(-1)\n    ~~~~~~~~~~~~~~~~~~\nValueError: factorial() not defined for negative values\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__floor_inf_error.py",
    "content": "import math\n\nmath.floor(float('inf'))\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__floor_inf_error.py\", line 3, in <module>\n    math.floor(float('inf'))\n    ~~~~~~~~~~~~~~~~~~~~~~~~\nOverflowError: cannot convert float infinity to integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__floor_nan_error.py",
    "content": "import math\n\nmath.floor(float('nan'))\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__floor_nan_error.py\", line 3, in <module>\n    math.floor(float('nan'))\n    ~~~~~~~~~~~~~~~~~~~~~~~~\nValueError: cannot convert float NaN to integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__floor_str_error.py",
    "content": "import math\n\nmath.floor('x')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__floor_str_error.py\", line 3, in <module>\n    math.floor('x')\n    ~~~~~~~~~~~~~~~\nTypeError: must be real number, not str\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__fmod_inf_error.py",
    "content": "import math\n\nmath.fmod(math.inf, 3)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__fmod_inf_error.py\", line 3, in <module>\n    math.fmod(math.inf, 3)\n    ~~~~~~~~~~~~~~~~~~~~~~\nValueError: math domain error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__gamma_neg_int_error.py",
    "content": "import math\n\nmath.gamma(0)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__gamma_neg_int_error.py\", line 3, in <module>\n    math.gamma(0)\n    ~~~~~~~~~~~~~\nValueError: expected a noninteger or positive integer, got 0.0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__gcd_float_error.py",
    "content": "import math\n\nmath.gcd(1.5, 2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__gcd_float_error.py\", line 3, in <module>\n    math.gcd(1.5, 2)\n    ~~~~~~~~~~~~~~~~\nTypeError: 'float' object cannot be interpreted as an integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__isqrt_negative_error.py",
    "content": "import math\n\nmath.isqrt(-1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__isqrt_negative_error.py\", line 3, in <module>\n    math.isqrt(-1)\n    ~~~~~~~~~~~~~~\nValueError: isqrt() argument must be nonnegative\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__ldexp_overflow_error.py",
    "content": "import math\n\nmath.ldexp(1.0, 1075)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__ldexp_overflow_error.py\", line 3, in <module>\n    math.ldexp(1.0, 1075)\n    ~~~~~~~~~~~~~~~~~~~~~\nOverflowError: math range error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__log1p_domain_error.py",
    "content": "import math\n\nmath.log1p(-2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__log1p_domain_error.py\", line 3, in <module>\n    math.log1p(-2)\n    ~~~~~~~~~~~~~~\nValueError: expected argument value > -1, got -2.0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__log_base1_error.py",
    "content": "import math\n\nmath.log(10, 1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__log_base1_error.py\", line 3, in <module>\n    math.log(10, 1)\n    ~~~~~~~~~~~~~~~\nZeroDivisionError: division by zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__log_zero_error.py",
    "content": "import math\n\nmath.log(0)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__log_zero_error.py\", line 3, in <module>\n    math.log(0)\n    ~~~~~~~~~~~\nValueError: expected a positive input\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__module.py",
    "content": "import math\n\n# === Constants ===\nassert math.pi == 3.141592653589793, 'math.pi value'\nassert math.e == 2.718281828459045, 'math.e value'\nassert math.tau == 6.283185307179586, 'math.tau value'\nassert math.inf == float('inf'), 'math.inf is infinity'\nassert math.nan != math.nan, 'math.nan is NaN (not equal to itself)'\nassert math.isinf(math.inf), 'math.inf is recognized by isinf'\nassert math.isnan(math.nan), 'math.nan is recognized by isnan'\n\n# === math.floor() ===\nassert math.floor(2.3) == 2, 'floor(2.3)'\nassert math.floor(-2.3) == -3, 'floor(-2.3)'\nassert math.floor(2.0) == 2, 'floor(2.0)'\nassert math.floor(5) == 5, 'floor(int)'\nassert math.floor(True) == 1, 'floor(True)'\nassert math.floor(False) == 0, 'floor(False)'\nassert math.floor(-0.5) == -1, 'floor(-0.5)'\nassert math.floor(0.9) == 0, 'floor(0.9)'\nassert math.floor(1e18) == 1000000000000000000, 'floor(1e18)'\n\nthrew = False\ntry:\n    math.floor(float('inf'))\nexcept OverflowError:\n    threw = True\nassert threw, 'floor(inf) raises OverflowError'\n\nthrew = False\ntry:\n    math.floor(float('nan'))\nexcept ValueError:\n    threw = True\nassert threw, 'floor(nan) raises ValueError'\n\nthrew = False\ntry:\n    math.floor('x')\nexcept TypeError:\n    threw = True\nassert threw, 'floor(str) raises TypeError'\n\n# === math.ceil() ===\nassert math.ceil(2.3) == 3, 'ceil(2.3)'\nassert math.ceil(-2.3) == -2, 'ceil(-2.3)'\nassert math.ceil(2.0) == 2, 'ceil(2.0)'\nassert math.ceil(5) == 5, 'ceil(int)'\nassert math.ceil(True) == 1, 'ceil(True)'\nassert math.ceil(False) == 0, 'ceil(False)'\nassert math.ceil(0.1) == 1, 'ceil(0.1)'\nassert math.ceil(-0.1) == 0, 'ceil(-0.1)'\n\nthrew = False\ntry:\n    math.ceil(float('inf'))\nexcept OverflowError:\n    threw = True\nassert threw, 'ceil(inf) raises OverflowError'\n\nthrew = False\ntry:\n    math.ceil(float('nan'))\nexcept ValueError:\n    threw = True\nassert threw, 'ceil(nan) raises ValueError'\n\nthrew = False\ntry:\n    math.ceil('x')\nexcept TypeError:\n    threw = True\nassert threw, 'ceil(str) raises TypeError'\n\n# === math.trunc() ===\nassert math.trunc(2.7) == 2, 'trunc(2.7)'\nassert math.trunc(-2.7) == -2, 'trunc(-2.7)'\nassert math.trunc(2.0) == 2, 'trunc(2.0)'\nassert math.trunc(5) == 5, 'trunc(int)'\nassert math.trunc(True) == 1, 'trunc(True)'\nassert math.trunc(False) == 0, 'trunc(False)'\n\nthrew = False\ntry:\n    math.trunc(float('inf'))\nexcept OverflowError:\n    threw = True\nassert threw, 'trunc(inf) raises OverflowError'\n\nthrew = False\ntry:\n    math.trunc(float('nan'))\nexcept ValueError:\n    threw = True\nassert threw, 'trunc(nan) raises ValueError'\n\nthrew = False\ntry:\n    math.trunc('x')\nexcept TypeError:\n    threw = True\nassert threw, 'trunc(str) raises TypeError'\n\n# === math.sqrt() ===\nassert math.sqrt(4) == 2.0, 'sqrt(4)'\nassert math.sqrt(2) == 1.4142135623730951, 'sqrt(2)'\nassert math.sqrt(0) == 0.0, 'sqrt(0)'\nassert math.sqrt(1) == 1.0, 'sqrt(1)'\nassert math.sqrt(0.25) == 0.5, 'sqrt(0.25)'\nassert isinstance(math.sqrt(4), float), 'sqrt returns float'\nassert math.sqrt(True) == 1.0, 'sqrt(True)'\nassert math.sqrt(False) == 0.0, 'sqrt(False)'\nassert math.sqrt(float('inf')) == float('inf'), 'sqrt(inf) returns inf'\nassert math.isnan(math.sqrt(float('nan'))), 'sqrt(nan) returns nan'\n\nthrew = False\ntry:\n    math.sqrt(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'sqrt(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.sqrt('x')\nexcept TypeError:\n    threw = True\nassert threw, 'sqrt(str) raises TypeError'\n\n# === math.isqrt() ===\nassert math.isqrt(0) == 0, 'isqrt(0)'\nassert math.isqrt(1) == 1, 'isqrt(1)'\nassert math.isqrt(4) == 2, 'isqrt(4)'\nassert math.isqrt(10) == 3, 'isqrt(10)'\nassert math.isqrt(99) == 9, 'isqrt(99)'\nassert math.isqrt(100) == 10, 'isqrt(100)'\nassert math.isqrt(True) == 1, 'isqrt(True)'\n\nthrew = False\ntry:\n    math.isqrt(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'isqrt(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.isqrt(4.0)\nexcept TypeError:\n    threw = True\nassert threw, 'isqrt(float) raises TypeError'\n\n# === math.cbrt() ===\nassert math.cbrt(0) == 0.0, 'cbrt(0)'\nassert math.cbrt(8) == 2.0, 'cbrt(8)'\nassert math.cbrt(-8) == -2.0, 'cbrt(-8)'\nassert math.cbrt(1) == 1.0, 'cbrt(1)'\nassert math.cbrt(64) == 4.0, 'cbrt(64)'\nassert math.cbrt(float('inf')) == float('inf'), 'cbrt(inf)'\nassert math.cbrt(float('-inf')) == float('-inf'), 'cbrt(-inf)'\nassert math.isnan(math.cbrt(float('nan'))), 'cbrt(nan) is nan'\n\nthrew = False\ntry:\n    math.cbrt('x')\nexcept TypeError:\n    threw = True\nassert threw, 'cbrt(str) raises TypeError'\n\n# === math.pow() ===\nassert math.pow(2, 3) == 8.0, 'pow(2, 3)'\nassert math.pow(2.0, 0.5) == math.sqrt(2), 'pow(2, 0.5)'\nassert math.pow(0, 0) == 1.0, 'pow(0, 0)'\nassert isinstance(math.pow(2, 3), float), 'pow returns float'\nassert math.pow(2, -1) == 0.5, 'pow(2, -1)'\nassert math.pow(float('inf'), 0) == 1.0, 'pow(inf, 0)'\nassert math.pow(float('nan'), 0) == 1.0, 'pow(nan, 0)'\nassert math.pow(1, float('inf')) == 1.0, 'pow(1, inf)'\nassert math.pow(1, float('nan')) == 1.0, 'pow(1, nan)'\n\nthrew = False\ntry:\n    math.pow(0, -1)\nexcept ValueError:\n    threw = True\nassert threw, 'pow(0, -1) raises ValueError'\n\nthrew = False\ntry:\n    math.pow(-1, 0.5)\nexcept ValueError:\n    threw = True\nassert threw, 'pow(-1, 0.5) raises ValueError'\n\nthrew = False\ntry:\n    math.pow(2, 1024)\nexcept OverflowError:\n    threw = True\nassert threw, 'pow(2, 1024) raises OverflowError'\n\nthrew = False\ntry:\n    math.pow('x', 2)\nexcept TypeError:\n    threw = True\nassert threw, 'pow(str, int) raises TypeError'\n\n# === math.exp() ===\nassert math.exp(0) == 1.0, 'exp(0)'\nassert math.exp(1) == math.e, 'exp(1)'\nassert math.exp(float('-inf')) == 0.0, 'exp(-inf)'\nassert math.exp(float('inf')) == float('inf'), 'exp(inf)'\nassert math.isnan(math.exp(float('nan'))), 'exp(nan) is nan'\n\nthrew = False\ntry:\n    math.exp(1000)\nexcept OverflowError:\n    threw = True\nassert threw, 'exp(1000) raises OverflowError'\n\nthrew = False\ntry:\n    math.exp('x')\nexcept TypeError:\n    threw = True\nassert threw, 'exp(str) raises TypeError'\n\n# === math.exp2() ===\nassert math.exp2(0) == 1.0, 'exp2(0)'\nassert math.exp2(3) == 8.0, 'exp2(3)'\nassert math.exp2(10) == 1024.0, 'exp2(10)'\nassert math.exp2(float('-inf')) == 0.0, 'exp2(-inf)'\nassert math.exp2(float('inf')) == float('inf'), 'exp2(inf)'\nassert math.isnan(math.exp2(float('nan'))), 'exp2(nan) is nan'\n\nthrew = False\ntry:\n    math.exp2(1024)\nexcept OverflowError:\n    threw = True\nassert threw, 'exp2(1024) raises OverflowError'\n\nthrew = False\ntry:\n    math.exp2('x')\nexcept TypeError:\n    threw = True\nassert threw, 'exp2(str) raises TypeError'\n\n# === math.expm1() ===\nassert math.expm1(0) == 0.0, 'expm1(0)'\nassert math.isclose(math.expm1(1), math.e - 1), 'expm1(1)'\nassert math.expm1(1e-15) != 0.0, 'expm1(1e-15) is precise'\nassert math.expm1(float('-inf')) == -1.0, 'expm1(-inf)'\nassert math.expm1(float('inf')) == float('inf'), 'expm1(inf)'\nassert math.isnan(math.expm1(float('nan'))), 'expm1(nan) is nan'\n\nthrew = False\ntry:\n    math.expm1(1000)\nexcept OverflowError:\n    threw = True\nassert threw, 'expm1(1000) raises OverflowError'\n\nthrew = False\ntry:\n    math.expm1('x')\nexcept TypeError:\n    threw = True\nassert threw, 'expm1(str) raises TypeError'\n\n# === math.fabs() ===\nassert math.fabs(-5) == 5.0, 'fabs(-5)'\nassert math.fabs(5) == 5.0, 'fabs(5)'\nassert math.fabs(-3.14) == 3.14, 'fabs(-3.14)'\nassert math.fabs(0) == 0.0, 'fabs(0)'\nassert isinstance(math.fabs(-5), float), 'fabs returns float'\nassert isinstance(math.fabs(0), float), 'fabs(0) returns float'\nassert math.fabs(True) == 1.0, 'fabs(True)'\nassert math.fabs(False) == 0.0, 'fabs(False)'\nassert math.fabs(float('inf')) == float('inf'), 'fabs(inf)'\nassert math.fabs(float('-inf')) == float('inf'), 'fabs(-inf)'\nassert math.isnan(math.fabs(float('nan'))), 'fabs(nan) returns nan'\n\nthrew = False\ntry:\n    math.fabs('x')\nexcept TypeError:\n    threw = True\nassert threw, 'fabs(str) raises TypeError'\n\n# === math.isnan() ===\nassert math.isnan(float('nan')) == True, 'isnan(nan)'\nassert math.isnan(1.0) == False, 'isnan(1.0)'\nassert math.isnan(0.0) == False, 'isnan(0.0)'\nassert math.isnan(float('inf')) == False, 'isnan(inf)'\nassert math.isnan(0) == False, 'isnan(int)'\nassert math.isnan(True) == False, 'isnan(True)'\nassert math.isnan(False) == False, 'isnan(False)'\n\nthrew = False\ntry:\n    math.isnan('x')\nexcept TypeError:\n    threw = True\nassert threw, 'isnan(str) raises TypeError'\n\n# === math.isinf() ===\nassert math.isinf(float('inf')) == True, 'isinf(inf)'\nassert math.isinf(float('-inf')) == True, 'isinf(-inf)'\nassert math.isinf(1.0) == False, 'isinf(1.0)'\nassert math.isinf(float('nan')) == False, 'isinf(nan)'\nassert math.isinf(0) == False, 'isinf(int)'\nassert math.isinf(True) == False, 'isinf(True)'\nassert math.isinf(False) == False, 'isinf(False)'\n\nthrew = False\ntry:\n    math.isinf('x')\nexcept TypeError:\n    threw = True\nassert threw, 'isinf(str) raises TypeError'\n\n# === math.isfinite() ===\nassert math.isfinite(1.0) == True, 'isfinite(1.0)'\nassert math.isfinite(0) == True, 'isfinite(0)'\nassert math.isfinite(float('inf')) == False, 'isfinite(inf)'\nassert math.isfinite(float('-inf')) == False, 'isfinite(-inf)'\nassert math.isfinite(float('nan')) == False, 'isfinite(nan)'\nassert math.isfinite(True) == True, 'isfinite(True)'\nassert math.isfinite(False) == True, 'isfinite(False)'\n\nthrew = False\ntry:\n    math.isfinite('x')\nexcept TypeError:\n    threw = True\nassert threw, 'isfinite(str) raises TypeError'\n\n# === math.copysign() ===\nassert math.copysign(1.0, -0.0) == -1.0, 'copysign(1.0, -0.0)'\nassert math.copysign(-1.0, 1.0) == 1.0, 'copysign(-1.0, 1.0)'\nassert math.copysign(5, -3) == -5.0, 'copysign(5, -3)'\nassert isinstance(math.copysign(5, -3), float), 'copysign returns float'\nassert math.copysign(float('inf'), -1.0) == float('-inf'), 'copysign(inf, -1.0)'\nassert math.copysign(0.0, -1.0) == -0.0, 'copysign(0.0, -1.0)'\nassert math.isnan(math.copysign(float('nan'), -1.0)), 'copysign(nan, -1.0) is nan'\nassert math.copysign(True, -1) == -1.0, 'copysign(True, -1)'\n\nthrew = False\ntry:\n    math.copysign('x', 1)\nexcept TypeError:\n    threw = True\nassert threw, 'copysign(str, int) raises TypeError'\n\n# === math.isclose() ===\nassert math.isclose(1.0, 1.0) == True, 'isclose equal'\nassert math.isclose(1.0, 1.0000000001) == True, 'isclose very close'\nassert math.isclose(1.0, 1.1) == False, 'isclose not close'\nassert math.isclose(0.0, 0.0) == True, 'isclose zeros'\nassert math.isclose(-0.0, 0.0) == True, 'isclose neg zero and zero'\nassert math.isclose(float('inf'), float('inf')) == True, 'isclose(inf, inf)'\nassert math.isclose(float('inf'), 1e308) == False, 'isclose(inf, large) is False'\nassert math.isclose(float('nan'), float('nan')) == False, 'isclose(nan, nan) is False'\nassert math.isclose(1e-15, 0.0) == False, 'isclose(1e-15, 0.0) is False with default abs_tol'\nassert math.isclose(0.0, 1e-15) == False, 'isclose(0.0, 1e-15) is False with default abs_tol'\n\nthrew = False\ntry:\n    math.isclose('x', 1)\nexcept TypeError:\n    threw = True\nassert threw, 'isclose(str, int) raises TypeError'\n\n# === math.log() ===\nassert math.log(1) == 0.0, 'log(1)'\nassert math.log(math.e) == 1.0, 'log(e)'\nassert math.log(100, 10) == 2.0, 'log(100, 10)'\nassert math.log(1, 10) == 0.0, 'log(1, 10)'\nassert math.log(True) == 0.0, 'log(True)'\nassert math.log(float('inf')) == float('inf'), 'log(inf) returns inf'\nassert math.isnan(math.log(float('nan'))), 'log(nan) returns nan'\nassert math.isnan(math.log(float('nan'), 2)), 'log(nan, 2) returns nan'\nassert math.log(float('inf'), 2) == float('inf'), 'log(inf, 2) returns inf'\n\nthrew = False\ntry:\n    math.log(0)\nexcept ValueError:\n    threw = True\nassert threw, 'log(0) raises ValueError'\n\nthrew = False\ntry:\n    math.log(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'log(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.log(10, 1)\nexcept ZeroDivisionError:\n    threw = True\nassert threw, 'log(10, 1) raises ZeroDivisionError'\n\nthrew = False\ntry:\n    math.log(10, 0)\nexcept ValueError:\n    threw = True\nassert threw, 'log(10, 0) raises ValueError'\n\nthrew = False\ntry:\n    math.log(10, -1)\nexcept ValueError:\n    threw = True\nassert threw, 'log(10, -1) raises ValueError'\n\nthrew = False\ntry:\n    math.log('x')\nexcept TypeError:\n    threw = True\nassert threw, 'log(str) raises TypeError'\n\n# === math.log2() ===\nassert math.log2(1) == 0.0, 'log2(1)'\nassert math.log2(8) == 3.0, 'log2(8)'\nassert math.log2(1024) == 10.0, 'log2(1024)'\nassert math.log2(True) == 0.0, 'log2(True)'\nassert math.log2(float('inf')) == float('inf'), 'log2(inf) returns inf'\nassert math.isnan(math.log2(float('nan'))), 'log2(nan) returns nan'\n\nthrew = False\ntry:\n    math.log2(0)\nexcept ValueError:\n    threw = True\nassert threw, 'log2(0) raises ValueError'\n\nthrew = False\ntry:\n    math.log2(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'log2(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.log2('x')\nexcept TypeError:\n    threw = True\nassert threw, 'log2(str) raises TypeError'\n\n# === math.log10() ===\nassert math.log10(1) == 0.0, 'log10(1)'\nassert math.log10(1000) == 3.0, 'log10(1000)'\nassert math.log10(100) == 2.0, 'log10(100)'\nassert math.log10(True) == 0.0, 'log10(True)'\nassert math.log10(float('inf')) == float('inf'), 'log10(inf) returns inf'\nassert math.isnan(math.log10(float('nan'))), 'log10(nan) returns nan'\n\nthrew = False\ntry:\n    math.log10(0)\nexcept ValueError:\n    threw = True\nassert threw, 'log10(0) raises ValueError'\n\nthrew = False\ntry:\n    math.log10(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'log10(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.log10('x')\nexcept TypeError:\n    threw = True\nassert threw, 'log10(str) raises TypeError'\n\n# === math.log1p() ===\nassert math.log1p(0) == 0.0, 'log1p(0)'\nassert math.isclose(math.log1p(math.e - 1), 1.0), 'log1p(e-1)'\nassert math.log1p(float('inf')) == float('inf'), 'log1p(inf)'\nassert math.isnan(math.log1p(float('nan'))), 'log1p(nan) is nan'\n\nthrew = False\ntry:\n    math.log1p(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'log1p(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.log1p(-2)\nexcept ValueError:\n    threw = True\nassert threw, 'log1p(-2) raises ValueError'\n\nthrew = False\ntry:\n    math.log1p('x')\nexcept TypeError:\n    threw = True\nassert threw, 'log1p(str) raises TypeError'\n\n# === math.factorial() ===\nassert math.factorial(0) == 1, 'factorial(0)'\nassert math.factorial(1) == 1, 'factorial(1)'\nassert math.factorial(5) == 120, 'factorial(5)'\nassert math.factorial(10) == 3628800, 'factorial(10)'\nassert math.factorial(20) == 2432902008176640000, 'factorial(20)'\nassert math.factorial(True) == 1, 'factorial(True)'\nassert math.factorial(False) == 1, 'factorial(False)'\n\nthrew = False\ntry:\n    math.factorial(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'factorial(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.factorial(1.5)\nexcept TypeError:\n    threw = True\nassert threw, 'factorial(1.5) raises TypeError'\n\nthrew = False\ntry:\n    math.factorial('x')\nexcept TypeError:\n    threw = True\nassert threw, 'factorial(str) raises TypeError'\n\n# === math.gcd() ===\nassert math.gcd(12, 8) == 4, 'gcd(12, 8)'\nassert math.gcd(0, 5) == 5, 'gcd(0, 5)'\nassert math.gcd(5, 0) == 5, 'gcd(5, 0)'\nassert math.gcd(0, 0) == 0, 'gcd(0, 0)'\nassert math.gcd(-12, 8) == 4, 'gcd(-12, 8)'\nassert math.gcd(12, -8) == 4, 'gcd(12, -8)'\nassert math.gcd(-12, -8) == 4, 'gcd(-12, -8)'\nassert math.gcd(7, 13) == 1, 'gcd(7, 13) coprime'\nassert math.gcd(True, 2) == 1, 'gcd(True, 2)'\nassert math.gcd(False, 5) == 5, 'gcd(False, 5)'\n\nthrew = False\ntry:\n    math.gcd(1.5, 2)\nexcept TypeError:\n    threw = True\nassert threw, 'gcd(float, int) raises TypeError'\n\nthrew = False\ntry:\n    math.gcd(2, 1.5)\nexcept TypeError:\n    threw = True\nassert threw, 'gcd(int, float) raises TypeError'\n\n# === math.lcm() ===\nassert math.lcm(4, 6) == 12, 'lcm(4, 6)'\nassert math.lcm(0, 5) == 0, 'lcm(0, 5)'\nassert math.lcm(5, 0) == 0, 'lcm(5, 0)'\nassert math.lcm(0, 0) == 0, 'lcm(0, 0)'\nassert math.lcm(3, 7) == 21, 'lcm(3, 7) coprime'\nassert math.lcm(6, 6) == 6, 'lcm(6, 6) equal'\nassert math.lcm(-4, 6) == 12, 'lcm(-4, 6) negative'\nassert math.lcm(-4, -6) == 12, 'lcm(-4, -6) both negative'\nassert math.lcm(True, 2) == 2, 'lcm(True, 2)'\nassert math.lcm(False, 5) == 0, 'lcm(False, 5)'\n\nthrew = False\ntry:\n    math.lcm(1.5, 2)\nexcept TypeError:\n    threw = True\nassert threw, 'lcm(float, int) raises TypeError'\n\nthrew = False\ntry:\n    math.lcm(2, 1.5)\nexcept TypeError:\n    threw = True\nassert threw, 'lcm(int, float) raises TypeError'\n\n# === math.comb() ===\nassert math.comb(5, 2) == 10, 'comb(5, 2)'\nassert math.comb(10, 0) == 1, 'comb(10, 0)'\nassert math.comb(10, 10) == 1, 'comb(10, 10)'\nassert math.comb(0, 0) == 1, 'comb(0, 0)'\nassert math.comb(5, 6) == 0, 'comb(5, 6) k > n'\n\nthrew = False\ntry:\n    math.comb(5, -1)\nexcept ValueError:\n    threw = True\nassert threw, 'comb(5, -1) raises ValueError'\n\nthrew = False\ntry:\n    math.comb(-1, 2)\nexcept ValueError:\n    threw = True\nassert threw, 'comb(-1, 2) raises ValueError'\n\nthrew = False\ntry:\n    math.comb(5.0, 2)\nexcept TypeError:\n    threw = True\nassert threw, 'comb(float, int) raises TypeError'\n\n# === math.perm() ===\nassert math.perm(5, 2) == 20, 'perm(5, 2)'\nassert math.perm(5, 0) == 1, 'perm(5, 0)'\nassert math.perm(5, 5) == 120, 'perm(5, 5)'\nassert math.perm(5, 6) == 0, 'perm(5, 6) k > n'\n\nthrew = False\ntry:\n    math.perm(5, -1)\nexcept ValueError:\n    threw = True\nassert threw, 'perm(5, -1) raises ValueError'\n\nthrew = False\ntry:\n    math.perm(-1, 2)\nexcept ValueError:\n    threw = True\nassert threw, 'perm(-1, 2) raises ValueError'\n\nthrew = False\ntry:\n    math.perm(5.0, 2)\nexcept TypeError:\n    threw = True\nassert threw, 'perm(float, int) raises TypeError'\n\n# === math.copysign() (already above) ===\n\n# === math.isclose() (already above) ===\n\n# === math.degrees() ===\nassert math.degrees(0) == 0.0, 'degrees(0)'\nassert math.degrees(math.pi) == 180.0, 'degrees(pi)'\nassert math.degrees(math.tau) == 360.0, 'degrees(tau)'\nassert math.degrees(True) == math.degrees(1), 'degrees(True)'\nassert math.degrees(float('inf')) == float('inf'), 'degrees(inf)'\nassert math.degrees(float('-inf')) == float('-inf'), 'degrees(-inf)'\nassert math.isnan(math.degrees(float('nan'))), 'degrees(nan) is nan'\n\nthrew = False\ntry:\n    math.degrees('x')\nexcept TypeError:\n    threw = True\nassert threw, 'degrees(str) raises TypeError'\n\n# === math.radians() ===\nassert math.radians(0) == 0.0, 'radians(0)'\nassert math.radians(180) == math.pi, 'radians(180)'\nassert math.radians(360) == math.tau, 'radians(360)'\nassert math.radians(True) == math.radians(1), 'radians(True)'\nassert math.radians(float('inf')) == float('inf'), 'radians(inf)'\nassert math.radians(float('-inf')) == float('-inf'), 'radians(-inf)'\nassert math.isnan(math.radians(float('nan'))), 'radians(nan) is nan'\n\nthrew = False\ntry:\n    math.radians('x')\nexcept TypeError:\n    threw = True\nassert threw, 'radians(str) raises TypeError'\n\n# === math.sin() ===\nassert math.sin(0) == 0.0, 'sin(0)'\nassert math.sin(math.pi / 2) == 1.0, 'sin(pi/2)'\nassert math.sin(math.pi) < 1e-15, 'sin(pi) near zero'\nassert math.isnan(math.sin(float('nan'))), 'sin(nan) is nan'\n\nthrew = False\ntry:\n    math.sin(float('inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'sin(inf) raises ValueError'\n\nthrew = False\ntry:\n    math.sin(float('-inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'sin(-inf) raises ValueError'\n\nthrew = False\ntry:\n    math.sin('x')\nexcept TypeError:\n    threw = True\nassert threw, 'sin(str) raises TypeError'\n\n# === math.cos() ===\nassert math.cos(0) == 1.0, 'cos(0)'\nassert abs(math.cos(math.pi / 2)) < 1e-15, 'cos(pi/2) near zero'\nassert math.cos(math.pi) == -1.0, 'cos(pi)'\nassert math.isnan(math.cos(float('nan'))), 'cos(nan) is nan'\n\nthrew = False\ntry:\n    math.cos(float('inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'cos(inf) raises ValueError'\n\nthrew = False\ntry:\n    math.cos(float('-inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'cos(-inf) raises ValueError'\n\nthrew = False\ntry:\n    math.cos('x')\nexcept TypeError:\n    threw = True\nassert threw, 'cos(str) raises TypeError'\n\n# === math.tan() ===\nassert math.tan(0) == 0.0, 'tan(0)'\nassert abs(math.tan(math.pi / 4) - 1.0) < 1e-15, 'tan(pi/4) near 1'\nassert math.isnan(math.tan(float('nan'))), 'tan(nan) is nan'\n\nthrew = False\ntry:\n    math.tan(float('inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'tan(inf) raises ValueError'\n\nthrew = False\ntry:\n    math.tan(float('-inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'tan(-inf) raises ValueError'\n\nthrew = False\ntry:\n    math.tan('x')\nexcept TypeError:\n    threw = True\nassert threw, 'tan(str) raises TypeError'\n\n# === math.asin() ===\nassert math.asin(0) == 0.0, 'asin(0)'\nassert math.asin(1) == math.pi / 2, 'asin(1)'\nassert math.asin(-1) == -math.pi / 2, 'asin(-1)'\nassert math.isnan(math.asin(float('nan'))), 'asin(nan) is nan'\n\nthrew = False\ntry:\n    math.asin(2)\nexcept ValueError:\n    threw = True\nassert threw, 'asin(2) raises ValueError'\n\nthrew = False\ntry:\n    math.asin(-2)\nexcept ValueError:\n    threw = True\nassert threw, 'asin(-2) raises ValueError'\n\nthrew = False\ntry:\n    math.asin('x')\nexcept TypeError:\n    threw = True\nassert threw, 'asin(str) raises TypeError'\n\n# === math.acos() ===\nassert math.acos(1) == 0.0, 'acos(1)'\nassert math.acos(0) == math.pi / 2, 'acos(0)'\nassert math.acos(-1) == math.pi, 'acos(-1)'\nassert math.isnan(math.acos(float('nan'))), 'acos(nan) is nan'\n\nthrew = False\ntry:\n    math.acos(2)\nexcept ValueError:\n    threw = True\nassert threw, 'acos(2) raises ValueError'\n\nthrew = False\ntry:\n    math.acos(-2)\nexcept ValueError:\n    threw = True\nassert threw, 'acos(-2) raises ValueError'\n\nthrew = False\ntry:\n    math.acos('x')\nexcept TypeError:\n    threw = True\nassert threw, 'acos(str) raises TypeError'\n\n# === math.atan() ===\nassert math.atan(0) == 0.0, 'atan(0)'\nassert math.atan(1) == math.pi / 4, 'atan(1)'\nassert math.atan(float('inf')) == math.pi / 2, 'atan(inf)'\nassert math.atan(float('-inf')) == -math.pi / 2, 'atan(-inf)'\nassert math.isnan(math.atan(float('nan'))), 'atan(nan) is nan'\n\nthrew = False\ntry:\n    math.atan('x')\nexcept TypeError:\n    threw = True\nassert threw, 'atan(str) raises TypeError'\n\n# === math.atan2() ===\nassert math.atan2(0, 1) == 0.0, 'atan2(0, 1)'\nassert math.atan2(1, 0) == math.pi / 2, 'atan2(1, 0)'\nassert math.atan2(0, -1) == math.pi, 'atan2(0, -1)'\nassert math.atan2(0, 0) == 0.0, 'atan2(0, 0)'\nassert math.atan2(-1, 0) == -math.pi / 2, 'atan2(-1, 0)'\nassert math.isclose(math.atan2(float('inf'), float('inf')), math.pi / 4), 'atan2(inf, inf)'\nassert math.isnan(math.atan2(float('nan'), 1)), 'atan2(nan, 1) is nan'\nassert math.isnan(math.atan2(1, float('nan'))), 'atan2(1, nan) is nan'\n\nthrew = False\ntry:\n    math.atan2('x', 1)\nexcept TypeError:\n    threw = True\nassert threw, 'atan2(str, int) raises TypeError'\n\n# === math.sinh() ===\nassert math.sinh(0) == 0.0, 'sinh(0)'\nassert math.isclose(math.sinh(1), 1.1752011936438014), 'sinh(1)'\nassert math.sinh(float('inf')) == float('inf'), 'sinh(inf)'\nassert math.sinh(float('-inf')) == float('-inf'), 'sinh(-inf)'\nassert math.isnan(math.sinh(float('nan'))), 'sinh(nan) is nan'\n\nthrew = False\ntry:\n    math.sinh(1000)\nexcept OverflowError:\n    threw = True\nassert threw, 'sinh(1000) raises OverflowError'\n\nthrew = False\ntry:\n    math.sinh('x')\nexcept TypeError:\n    threw = True\nassert threw, 'sinh(str) raises TypeError'\n\n# === math.cosh() ===\nassert math.cosh(0) == 1.0, 'cosh(0)'\nassert math.isclose(math.cosh(1), 1.5430806348152437), 'cosh(1)'\nassert math.cosh(float('inf')) == float('inf'), 'cosh(inf)'\nassert math.cosh(float('-inf')) == float('inf'), 'cosh(-inf)'\nassert math.isnan(math.cosh(float('nan'))), 'cosh(nan) is nan'\n\nthrew = False\ntry:\n    math.cosh(1000)\nexcept OverflowError:\n    threw = True\nassert threw, 'cosh(1000) raises OverflowError'\n\nthrew = False\ntry:\n    math.cosh('x')\nexcept TypeError:\n    threw = True\nassert threw, 'cosh(str) raises TypeError'\n\n# === math.tanh() ===\nassert math.tanh(0) == 0.0, 'tanh(0)'\nassert math.tanh(float('inf')) == 1.0, 'tanh(inf)'\nassert math.tanh(float('-inf')) == -1.0, 'tanh(-inf)'\nassert math.tanh(1) == 0.7615941559557649, 'tanh(1)'\nassert math.isnan(math.tanh(float('nan'))), 'tanh(nan) is nan'\n\nthrew = False\ntry:\n    math.tanh('x')\nexcept TypeError:\n    threw = True\nassert threw, 'tanh(str) raises TypeError'\n\n# === math.asinh() ===\nassert math.asinh(0) == 0.0, 'asinh(0)'\nassert math.isclose(math.asinh(1), 0.881373587019543), 'asinh(1)'\nassert math.asinh(float('inf')) == float('inf'), 'asinh(inf)'\nassert math.asinh(float('-inf')) == float('-inf'), 'asinh(-inf)'\nassert math.isnan(math.asinh(float('nan'))), 'asinh(nan) is nan'\n\nthrew = False\ntry:\n    math.asinh('x')\nexcept TypeError:\n    threw = True\nassert threw, 'asinh(str) raises TypeError'\n\n# === math.acosh() ===\nassert math.acosh(1) == 0.0, 'acosh(1)'\nassert math.isclose(math.acosh(2), 1.3169578969248166), 'acosh(2)'\nassert math.acosh(float('inf')) == float('inf'), 'acosh(inf)'\nassert math.isnan(math.acosh(float('nan'))), 'acosh(nan) is nan'\n\nthrew = False\ntry:\n    math.acosh(0.5)\nexcept ValueError:\n    threw = True\nassert threw, 'acosh(0.5) raises ValueError'\n\nthrew = False\ntry:\n    math.acosh('x')\nexcept TypeError:\n    threw = True\nassert threw, 'acosh(str) raises TypeError'\n\n# === math.atanh() ===\nassert math.atanh(0) == 0.0, 'atanh(0)'\nassert math.isclose(math.atanh(0.5), 0.5493061443340549), 'atanh(0.5)'\nassert math.isnan(math.atanh(float('nan'))), 'atanh(nan) is nan'\n\nthrew = False\ntry:\n    math.atanh(1)\nexcept ValueError:\n    threw = True\nassert threw, 'atanh(1) raises ValueError'\n\nthrew = False\ntry:\n    math.atanh(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'atanh(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.atanh('x')\nexcept TypeError:\n    threw = True\nassert threw, 'atanh(str) raises TypeError'\n\n# === math.fmod() ===\nassert math.fmod(10, 3) == 1.0, 'fmod(10, 3)'\nassert math.fmod(-10, 3) == -1.0, 'fmod(-10, 3)'\nassert math.fmod(10.5, 3) == 1.5, 'fmod(10.5, 3)'\nassert math.fmod(3, float('inf')) == 3.0, 'fmod(3, inf)'\nassert math.isnan(math.fmod(float('nan'), 3)), 'fmod(nan, 3) is nan'\nassert math.isnan(math.fmod(3, float('nan'))), 'fmod(3, nan) is nan'\nassert math.isnan(math.fmod(float('nan'), float('nan'))), 'fmod(nan, nan) is nan'\n\nthrew = False\ntry:\n    math.fmod(10, 0)\nexcept ValueError:\n    threw = True\nassert threw, 'fmod(10, 0) raises ValueError'\n\nthrew = False\ntry:\n    math.fmod(float('inf'), 3)\nexcept ValueError:\n    threw = True\nassert threw, 'fmod(inf, 3) raises ValueError'\n\nthrew = False\ntry:\n    math.fmod('x', 3)\nexcept TypeError:\n    threw = True\nassert threw, 'fmod(str, int) raises TypeError'\n\n# === math.remainder() ===\nassert math.remainder(10, 3) == 1.0, 'remainder(10, 3)'\nassert math.remainder(10, 4) == 2.0, 'remainder(10, 4)'\nassert math.remainder(-10, 3) == -1.0, 'remainder(-10, 3)'\nassert math.remainder(10.5, 3) == -1.5, 'remainder(10.5, 3)'\nassert math.remainder(3, float('inf')) == 3.0, 'remainder(3, inf)'\nassert math.isnan(math.remainder(float('nan'), 3)), 'remainder(nan, 3) is nan'\nassert math.isnan(math.remainder(3, float('nan'))), 'remainder(3, nan) is nan'\n\nthrew = False\ntry:\n    math.remainder(10, 0)\nexcept ValueError:\n    threw = True\nassert threw, 'remainder(10, 0) raises ValueError'\n\nthrew = False\ntry:\n    math.remainder(float('inf'), 3)\nexcept ValueError:\n    threw = True\nassert threw, 'remainder(inf, 3) raises ValueError'\n\nthrew = False\ntry:\n    math.remainder('x', 3)\nexcept TypeError:\n    threw = True\nassert threw, 'remainder(str, int) raises TypeError'\n\n# === math.modf() ===\nr = math.modf(3.5)\nassert r == (0.5, 3.0), 'modf(3.5)'\nr = math.modf(-3.5)\nassert r == (-0.5, -3.0), 'modf(-3.5)'\nr = math.modf(0.0)\nassert r == (0.0, 0.0), 'modf(0.0)'\nr = math.modf(float('inf'))\nassert r == (0.0, float('inf')), 'modf(inf)'\nr = math.modf(float('-inf'))\n# modf(-inf) returns (-0.0, -inf), verify both parts including sign of fractional\nassert str(r[0]) == '-0.0', 'modf(-inf) fractional part is -0.0'\nassert r[1] == float('-inf'), 'modf(-inf) integer part is -inf'\nr_nan = math.modf(float('nan'))\nassert math.isnan(r_nan[0]) and math.isnan(r_nan[1]), 'modf(nan) both parts are nan'\n\nthrew = False\ntry:\n    math.modf('x')\nexcept TypeError:\n    threw = True\nassert threw, 'modf(str) raises TypeError'\n\n# === math.frexp() ===\nr = math.frexp(0.0)\nassert r == (0.0, 0), 'frexp(0.0)'\nr = math.frexp(3.5)\nassert r == (0.875, 2), 'frexp(3.5)'\nr = math.frexp(1.0)\nassert r == (0.5, 1), 'frexp(1.0)'\nr = math.frexp(-1.0)\nassert r == (-0.5, 1), 'frexp(-1.0)'\nr = math.frexp(float('inf'))\nassert r == (float('inf'), 0), 'frexp(inf)'\nr = math.frexp(float('-inf'))\nassert r == (float('-inf'), 0), 'frexp(-inf)'\nr_nan = math.frexp(float('nan'))\nassert math.isnan(r_nan[0]) and r_nan[1] == 0, 'frexp(nan)'\n\nthrew = False\ntry:\n    math.frexp('x')\nexcept TypeError:\n    threw = True\nassert threw, 'frexp(str) raises TypeError'\n\n# === math.ldexp() ===\nassert math.ldexp(0.875, 2) == 3.5, 'ldexp(0.875, 2)'\nassert math.ldexp(1.0, 0) == 1.0, 'ldexp(1.0, 0)'\nassert math.ldexp(0.5, 1) == 1.0, 'ldexp(0.5, 1)'\nassert math.ldexp(1.0, -1075) == 0.0, 'ldexp(1.0, -1075) underflows to 0'\nassert math.ldexp(float('inf'), 1) == float('inf'), 'ldexp(inf, 1)'\nassert math.isnan(math.ldexp(float('nan'), 1)), 'ldexp(nan, 1) is nan'\nassert math.ldexp(0.0, 1000) == 0.0, 'ldexp(0.0, 1000)'\n\nthrew = False\ntry:\n    math.ldexp(1.0, 1075)\nexcept OverflowError:\n    threw = True\nassert threw, 'ldexp(1.0, 1075) raises OverflowError'\n\nthrew = False\ntry:\n    math.ldexp(0.5, 1025)\nexcept OverflowError:\n    threw = True\nassert threw, 'ldexp(0.5, 1025) raises OverflowError'\n\nthrew = False\ntry:\n    math.ldexp('x', 1)\nexcept TypeError:\n    threw = True\nassert threw, 'ldexp(str, int) raises TypeError'\n\n# === math.gamma() ===\nassert math.gamma(1) == 1.0, 'gamma(1)'\nassert math.gamma(5) == 24.0, 'gamma(5)'\nassert math.isclose(math.gamma(0.5), math.sqrt(math.pi)), 'gamma(0.5)'\nassert math.gamma(float('inf')) == float('inf'), 'gamma(inf)'\nassert math.isnan(math.gamma(float('nan'))), 'gamma(nan) is nan'\n\nthrew = False\ntry:\n    math.gamma(0)\nexcept ValueError:\n    threw = True\nassert threw, 'gamma(0) raises ValueError'\n\nthrew = False\ntry:\n    math.gamma(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'gamma(-1) raises ValueError'\n\nthrew = False\ntry:\n    math.gamma(float('-inf'))\nexcept ValueError:\n    threw = True\nassert threw, 'gamma(-inf) raises ValueError'\n\nthrew = False\ntry:\n    math.gamma(172)\nexcept OverflowError:\n    threw = True\nassert threw, 'gamma(172) raises OverflowError'\n\nthrew = False\ntry:\n    math.gamma('x')\nexcept TypeError:\n    threw = True\nassert threw, 'gamma(str) raises TypeError'\n\n# === math.lgamma() ===\nassert math.lgamma(1) == 0.0, 'lgamma(1)'\nassert math.isclose(math.lgamma(5), math.log(24)), 'lgamma(5)'\nassert math.lgamma(float('inf')) == float('inf'), 'lgamma(inf)'\nassert math.isnan(math.lgamma(float('nan'))), 'lgamma(nan) is nan'\nassert math.isclose(math.lgamma(-0.5), 1.265512123484645), 'lgamma(-0.5)'\n\nthrew = False\ntry:\n    math.lgamma(0)\nexcept ValueError:\n    threw = True\nassert threw, 'lgamma(0) raises ValueError'\n\nthrew = False\ntry:\n    math.lgamma(-2)\nexcept ValueError:\n    threw = True\nassert threw, 'lgamma(-2) raises ValueError'\n\nthrew = False\ntry:\n    math.lgamma('x')\nexcept TypeError:\n    threw = True\nassert threw, 'lgamma(str) raises TypeError'\n\n# === math.erf() ===\nassert math.erf(0) == 0.0, 'erf(0)'\nassert math.isclose(math.erf(1), 0.8427007929497148, rel_tol=1e-15), 'erf(1)'\nassert math.isclose(math.erf(-1), -0.8427007929497148, rel_tol=1e-15), 'erf(-1)'\nassert math.erf(float('inf')) == 1.0, 'erf(inf)'\nassert math.erf(float('-inf')) == -1.0, 'erf(-inf)'\nassert math.isnan(math.erf(float('nan'))), 'erf(nan) is nan'\n\nthrew = False\ntry:\n    math.erf('x')\nexcept TypeError:\n    threw = True\nassert threw, 'erf(str) raises TypeError'\n\n# === math.erfc() ===\nassert math.erfc(0) == 1.0, 'erfc(0)'\nassert math.isclose(math.erfc(1), 1.0 - math.erf(1)), 'erfc(1)'\nassert math.erfc(float('inf')) == 0.0, 'erfc(inf)'\nassert math.erfc(float('-inf')) == 2.0, 'erfc(-inf)'\nassert math.isnan(math.erfc(float('nan'))), 'erfc(nan) is nan'\n\nthrew = False\ntry:\n    math.erfc('x')\nexcept TypeError:\n    threw = True\nassert threw, 'erfc(str) raises TypeError'\n\n# === math.nextafter() ===\nr = math.nextafter(1.0, 2.0)\nassert r > 1.0, 'nextafter(1.0, 2.0) > 1.0'\nassert r == 1.0000000000000002, 'nextafter(1.0, 2.0) value'\nr = math.nextafter(1.0, 0.0)\nassert r < 1.0, 'nextafter(1.0, 0.0) < 1.0'\nassert math.nextafter(0.0, 1.0) == 5e-324, 'nextafter(0.0, 1.0) smallest positive'\nassert math.nextafter(0.0, -1.0) == -5e-324, 'nextafter(0.0, -1.0) smallest negative'\nassert math.isnan(math.nextafter(float('nan'), 1.0)), 'nextafter(nan, 1.0) is nan'\nassert math.isnan(math.nextafter(1.0, float('nan'))), 'nextafter(1.0, nan) is nan'\nassert math.nextafter(float('inf'), float('inf')) == float('inf'), 'nextafter(inf, inf)'\nassert math.nextafter(1.0, 1.0) == 1.0, 'nextafter(1.0, 1.0) equal inputs'\n\nthrew = False\ntry:\n    math.nextafter('x', 1.0)\nexcept TypeError:\n    threw = True\nassert threw, 'nextafter(str, float) raises TypeError'\n\n# === math.ulp() ===\nassert math.ulp(1.0) == 2.220446049250313e-16, 'ulp(1.0)'\nassert math.ulp(-1.0) == 2.220446049250313e-16, 'ulp(-1.0) same as ulp(1.0)'\nassert math.ulp(0.0) == 5e-324, 'ulp(0.0) is smallest subnormal'\nassert math.isinf(math.ulp(float('inf'))), 'ulp(inf) is inf'\nassert math.isnan(math.ulp(float('nan'))), 'ulp(nan) is nan'\nassert math.ulp(5e-324) == 5e-324, 'ulp(smallest subnormal)'\n\nthrew = False\ntry:\n    math.ulp('x')\nexcept TypeError:\n    threw = True\nassert threw, 'ulp(str) raises TypeError'\n\n# === Additional edge cases for coverage ===\n\n# --- frexp subnormal numbers ---\nr = math.frexp(5e-324)\nassert r == (0.5, -1073), 'frexp(5e-324) subnormal'\n\n# --- ldexp large negative exponent (underflow to zero) ---\nassert math.ldexp(1.0, -2000) == 0.0, 'ldexp(1.0, -2000) underflows to 0'\n\n# --- fmod NaN propagation edge cases ---\nassert math.isnan(math.fmod(float('inf'), float('nan'))), 'fmod(inf, nan) propagates nan'\nassert math.isnan(math.fmod(float('nan'), 0)), 'fmod(nan, 0) propagates nan'\n\n# --- gamma negative non-integer (reflection formula) ---\nassert math.isclose(math.gamma(-0.5), -3.544907701811032), 'gamma(-0.5)'\nassert math.isclose(math.gamma(-1.5), 2.3632718012073544), 'gamma(-1.5)'\n\n# --- lgamma(-inf) returns inf ---\nassert math.lgamma(float('-inf')) == float('inf'), 'lgamma(-inf) returns inf'\n\n# --- lgamma overflow for extremely large input ---\nthrew = False\ntry:\n    math.lgamma(1e308)\nexcept OverflowError:\n    threw = True\nassert threw, 'lgamma(1e308) raises OverflowError'\n\n# --- lgamma negative non-integer (reflection formula) ---\nassert math.isclose(math.lgamma(-0.5), 1.265512123484645), 'lgamma(-0.5) reflection'\n\n# ==========================================================\n# Tests for bug fixes and CPython behavior alignment\n# ==========================================================\n\n# === floor/ceil/trunc with large floats (LongInt promotion) ===\nlarge_floor = math.floor(1e300)\nassert large_floor > 0, 'floor(1e300) should be positive'\nassert (\n    large_floor\n    == 1000000000000000052504760255204420248704468581108159154915854115511802457988908195786371375080447864043704443832883878176942523235360430575644792184786706982848387200926575803737830233794788090059368953234970799945081119038967640880074652742780142494579258788820056842838115669472196386865459400540160\n), 'floor(1e300) matches CPython'\n\nlarge_ceil = math.ceil(-1e300)\nassert large_ceil < 0, 'ceil(-1e300) should be negative'\nassert (\n    large_ceil\n    == -1000000000000000052504760255204420248704468581108159154915854115511802457988908195786371375080447864043704443832883878176942523235360430575644792184786706982848387200926575803737830233794788090059368953234970799945081119038967640880074652742780142494579258788820056842838115669472196386865459400540160\n), 'ceil(-1e300) matches CPython'\n\nlarge_trunc = math.trunc(1e300)\nassert large_trunc == math.floor(1e300), 'trunc(1e300) matches floor(1e300) for positive'\nlarge_trunc_neg = math.trunc(-1e300)\nassert large_trunc_neg == math.ceil(-1e300), 'trunc(-1e300) matches ceil(-1e300) for negative'\n\n# floor/ceil should still work normally for values within i64 range\nassert math.floor(1e18) == 1000000000000000000, 'floor(1e18) within i64 range'\nassert math.floor(2.7) == 2, 'floor(2.7) basic case'\nassert math.ceil(-2.7) == -2, 'ceil(-2.7) basic case'\n\n# === ldexp with large exponent but small x ===\nassert math.ldexp(5e-324, 1075) == 2.0, 'ldexp(5e-324, 1075) should be 2.0'\nassert math.ldexp(0.5, 1024) == 8.98846567431158e307, 'ldexp(0.5, 1024) large but finite'\n\n# === modf(-0.0) sign preservation ===\nfrac, integer = math.modf(-0.0)\n# Both parts should be -0.0\nassert str(frac) == '-0.0', 'modf(-0.0) fractional part is -0.0'\nassert str(integer) == '-0.0', 'modf(-0.0) integer part is -0.0'\n\n# === erfc accuracy for large x ===\nerfc_6 = math.erfc(6)\nassert erfc_6 > 0, 'erfc(6) should be positive, not zero'\nassert math.isclose(erfc_6, 2.1519736712498913e-17, rel_tol=1e-12), 'erfc(6) matches CPython'\nerfc_neg6 = math.erfc(-6)\nassert erfc_neg6 == 2.0, 'erfc(-6) is exactly 2.0'\nassert math.erfc(0) == 1.0, 'erfc(0) is 1.0'\n\n# === variadic gcd ===\nassert math.gcd() == 0, 'gcd() with no args returns 0'\nassert math.gcd(12) == 12, 'gcd(12) single arg returns abs(12)'\nassert math.gcd(-12) == 12, 'gcd(-12) single arg returns abs(-12)'\nassert math.gcd(12, 8) == 4, 'gcd(12, 8) two args'\nassert math.gcd(12, 8, 6) == 2, 'gcd(12, 8, 6) three args'\n\n# === variadic lcm ===\nassert math.lcm() == 1, 'lcm() with no args returns 1'\nassert math.lcm(12) == 12, 'lcm(12) single arg returns abs(12)'\nassert math.lcm(-12) == 12, 'lcm(-12) single negative arg returns abs(-12)'\nassert math.lcm(4, 6) == 12, 'lcm(4, 6) two args'\nassert math.lcm(4, 6, 10) == 60, 'lcm(4, 6, 10) three args'\nassert math.lcm(0, 5) == 0, 'lcm(0, 5) returns 0 if any arg is 0'\n\n# === perm with optional k ===\nassert math.perm(5) == 120, 'perm(5) defaults k to n (= 5!)'\nassert math.perm(5, 2) == 20, 'perm(5, 2) with explicit k'\nassert math.perm(0) == 1, 'perm(0) is 1'\n\n# === isclose with rel_tol/abs_tol kwargs ===\nassert math.isclose(1.0, 1.1, rel_tol=0.2) == True, 'isclose with rel_tol=0.2'\nassert math.isclose(1.0, 1.1, abs_tol=0.2) == True, 'isclose with abs_tol=0.2'\nassert math.isclose(1.0, 1.1) == False, 'isclose with defaults (not close)'\nassert math.isclose(1.0, 1.0 + 1e-10) == True, 'isclose with defaults (close)'\n\n# isclose negative tolerance raises ValueError\nthrew = False\ntry:\n    math.isclose(1.0, 1.0, rel_tol=-0.1)\nexcept ValueError:\n    threw = True\nassert threw, 'isclose with negative rel_tol raises ValueError'\n\nthrew = False\ntry:\n    math.isclose(1.0, 1.0, abs_tol=-0.1)\nexcept ValueError:\n    threw = True\nassert threw, 'isclose with negative abs_tol raises ValueError'\n\n# isclose unknown kwarg raises TypeError\nthrew = False\ntry:\n    math.isclose(1.0, 1.0, foo=0.1)\nexcept TypeError:\n    threw = True\nassert threw, 'isclose with unknown kwarg raises TypeError'\n\n# === ldexp sign preservation ===\nassert str(math.ldexp(-0.0, 1000)) == '-0.0', 'ldexp(-0.0, n) preserves sign'\nassert math.ldexp(float('-inf'), 1) == float('-inf'), 'ldexp(-inf, 1) returns -inf'\n\n# === frexp(-0.0) sign preservation ===\nm, e = math.frexp(-0.0)\nassert str(m) == '-0.0', 'frexp(-0.0) mantissa preserves sign'\nassert e == 0, 'frexp(-0.0) exponent is 0'\n\n# === comb with GCD reduction (values that would overflow intermediate without it) ===\nassert math.comb(62, 31) == 465428353255261088, 'comb(62, 31) with GCD reduction'\nassert math.comb(61, 30) == 232714176627630544, 'comb(61, 30) with GCD reduction'\n\n# === isclose arg count errors ===\nthrew = False\ntry:\n    math.isclose()\nexcept TypeError:\n    threw = True\nassert threw, 'isclose with 0 args raises TypeError'\n\nthrew = False\ntry:\n    math.isclose(1.0)\nexcept TypeError:\n    threw = True\nassert threw, 'isclose with 1 arg raises TypeError'\n\nthrew = False\ntry:\n    math.isclose(1.0, 2.0, 3.0)\nexcept TypeError:\n    threw = True\nassert threw, 'isclose with 3 positional args raises TypeError'\n\n# === perm(-1) single-arg error message ===\nthrew = False\ntry:\n    math.perm(-1)\nexcept ValueError:\n    threw = True\nassert threw, 'perm(-1) single-arg raises ValueError'\n\n# === gcd/lcm with i64::MIN-like values (u64 promotion) ===\n# gcd(-9223372036854775808, 0) should return 9223372036854775808 (exceeds i64::MAX)\nbig_gcd = math.gcd(-9223372036854775808, 0)\nassert big_gcd == 9223372036854775808, 'gcd(i64::MIN, 0) promotes to LongInt'\n\n# === isqrt large values (Newton's method refinement) ===\n# Values near i64::MAX where f64 sqrt loses precision\nassert math.isqrt(9223372036854775807) == 3037000499, 'isqrt(i64::MAX)'\nassert math.isqrt(9223372030926249001) == 3037000499, 'isqrt(3037000499^2)'\nassert math.isqrt(9223372030926249000) == 3037000498, 'isqrt(3037000499^2 - 1)'\n\n# === erf/erfc range coverage ===\n# Small x (|x| < 0.84375): exercises PP/QQ polynomial\nassert math.erf(0.1) == 0.1124629160182849, 'erf(0.1) small-x range'\nassert math.erf(0.5) == 0.5204998778130465, 'erf(0.5) small-x range'\n\n# Medium x (1.25 ≤ |x| < 28): exercises erfc_inner path\nassert math.erf(2.0) == 0.9953222650189527, 'erf(2.0) medium-x range'\nassert math.erf(5.0) == 0.9999999999984626, 'erf(5.0) large-x range'\n\n# erfc in range 3 (1.25 ≤ |x| < 2.857): exercises RA/SA coefficients\nerfc_2 = math.erfc(2.0)\nassert math.isclose(erfc_2, 0.004677734981047266, rel_tol=1e-12), 'erfc(2.0) range 3'\n"
  },
  {
    "path": "crates/monty/test_cases/math__pow_domain_error.py",
    "content": "import math\n\nmath.pow(-1, 0.5)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__pow_domain_error.py\", line 3, in <module>\n    math.pow(-1, 0.5)\n    ~~~~~~~~~~~~~~~~~\nValueError: math domain error\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__sin_inf_error.py",
    "content": "import math\n\nmath.sin(math.inf)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__sin_inf_error.py\", line 3, in <module>\n    math.sin(math.inf)\n    ~~~~~~~~~~~~~~~~~~\nValueError: expected a finite input, got inf\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__sqrt_negative_error.py",
    "content": "import math\n\nmath.sqrt(-1)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__sqrt_negative_error.py\", line 3, in <module>\n    math.sqrt(-1)\n    ~~~~~~~~~~~~~\nValueError: expected a nonnegative input, got -1.0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__tan_inf_error.py",
    "content": "import math\n\nmath.tan(math.inf)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__tan_inf_error.py\", line 3, in <module>\n    math.tan(math.inf)\n    ~~~~~~~~~~~~~~~~~~\nValueError: expected a finite input, got inf\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/math__trunc_str_error.py",
    "content": "import math\n\nmath.trunc('x')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"math__trunc_str_error.py\", line 3, in <module>\n    math.trunc('x')\n    ~~~~~~~~~~~~~~~\nTypeError: type str doesn't define __trunc__ method\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/method__args_kwargs_unpacking.py",
    "content": "# Tests for method calls with *args unpacking\n\n# === Basic *args unpacking ===\nitems = ['a', 'b', 'c']\nresult = '-'.join(*[items])\nassert result == 'a-b-c', f'join with *args: {result}'\n\nparts = ['hello', 'world']\nresult = ' '.join(*[parts])\nassert result == 'hello world', f'join with *args list: {result}'\n\n# === Empty *args unpacking ===\nresult = '-'.join(*[[]])\nassert result == '', f'join with empty *args: {result}'\n\nempty = []\nresult = '-'.join(*[empty])\nassert result == '', f'join with empty list via *args: {result}'\n\n# === *args with tuple unpacking ===\nvalues = ('x', 'y', 'z')\nresult = '|'.join(*[list(values)])\nassert result == 'x|y|z', f'join with tuple *args: {result}'\n\n# === String methods with *args ===\ns = 'hello world'\nargs = ('o', 'O')\nresult = s.replace(*args)\nassert result == 'hellO wOrld', f'replace with *args: {result}'\n\n# Count with *args\ncount_args = ('l',)\nresult = s.count(*count_args)\nassert result == 3, f'count with *args: {result}'\n\n# === List methods with *args ===\nmy_list = [1, 2, 3]\nappend_args = [4]\nmy_list.append(*append_args)\nassert my_list == [1, 2, 3, 4], f'append with *args: {my_list}'\n\nmy_list = [1, 2, 3]\nextend_args = [[4, 5]]\nmy_list.extend(*extend_args)\nassert my_list == [1, 2, 3, 4, 5], f'extend with *args: {my_list}'\n\nmy_list = [1, 2, 3]\ninsert_args = (1, 'x')\nmy_list.insert(*insert_args)\nassert my_list == [1, 'x', 2, 3], f'insert with *args: {my_list}'\n\n# === Dict methods with *args ===\nd = {'a': 1, 'b': 2}\nget_args = ('a',)\nresult = d.get(*get_args)\nassert result == 1, f'dict.get with *args: {result}'\n\nget_args_default = ('missing', 'default')\nresult = d.get(*get_args_default)\nassert result == 'default', f'dict.get with *args and default: {result}'\n\n# === Mixed positional and *args ===\nmy_list = [1, 2, 3]\nextra_args = ('y',)\nmy_list.insert(0, *extra_args)\nassert my_list == ['y', 1, 2, 3], f'insert with pos and *args: {my_list}'\n\n# === setdefault with *args ===\nd = {'a': 1}\nargs = ('b', 2)\nresult = d.setdefault(*args)\nassert result == 2, f'setdefault with *args: {result}'\nassert d == {'a': 1, 'b': 2}, f'dict after setdefault: {d}'\n\n# === pop with *args ===\nd = {'a': 1, 'b': 2}\npop_args = ('a',)\nresult = d.pop(*pop_args)\nassert result == 1, f'pop with *args: {result}'\nassert d == {'b': 2}, f'dict after pop: {d}'\n\npop_args_default = ('missing', 'default')\nresult = d.pop(*pop_args_default)\nassert result == 'default', f'pop with *args and default: {result}'\n\n# === String split with *args ===\ns = 'a,b,c,d'\nsplit_args = (',',)\nresult = s.split(*split_args)\nassert result == ['a', 'b', 'c', 'd'], f'split with *args: {result}'\n\nsplit_args_maxsplit = (',', 2)\nresult = s.split(*split_args_maxsplit)\nassert result == ['a', 'b', 'c,d'], f'split with *args maxsplit: {result}'\n\n# === String startswith/endswith with *args ===\ns = 'hello'\nstartswith_args = (('hel', 'hey'),)\nresult = s.startswith(*startswith_args)\nassert result == True, f'startswith with *args tuple: {result}'\n\nendswith_args = ('lo',)\nresult = s.endswith(*endswith_args)\nassert result == True, f'endswith with *args: {result}'\n\n# === List index with *args ===\nmy_list = [1, 2, 3, 2, 4]\nindex_args = (2,)\nresult = my_list.index(*index_args)\nassert result == 1, f'index with *args: {result}'\n\nindex_args_start = (2, 2)\nresult = my_list.index(*index_args_start)\nassert result == 3, f'index with *args and start: {result}'\n\n# === String find with *args ===\ns = 'hello hello'\nfind_args = ('hello',)\nresult = s.find(*find_args)\nassert result == 0, f'find with *args: {result}'\n\nfind_args_start = ('hello', 1)\nresult = s.find(*find_args_start)\nassert result == 6, f'find with *args and start: {result}'\n\n# ============================================================\n# **kwargs unpacking tests\n# ============================================================\n\n# === Basic **kwargs unpacking with dict.update ===\nd = {'a': 1}\nopts = {'b': 2, 'c': 3}\nd.update(**opts)\nassert d == {'a': 1, 'b': 2, 'c': 3}, f'update with **kwargs: {d}'\n\n# === Empty **kwargs unpacking ===\nd = {'a': 1}\nempty_opts = {}\nd.update(**empty_opts)\nassert d == {'a': 1}, f'update with empty **kwargs: {d}'\n\n# === **kwargs with string keys ===\nd = {}\nstr_opts = {'key1': 'value1', 'key2': 'value2'}\nd.update(**str_opts)\nassert d == {'key1': 'value1', 'key2': 'value2'}, f'update with string **kwargs: {d}'\n\n# === **kwargs with heap-allocated values ===\nd = {}\nlist_val = [1, 2, 3]\ndict_val = {'nested': True}\nheap_opts = {'list': list_val, 'dict': dict_val}\nd.update(**heap_opts)\nassert d['list'] == [1, 2, 3], f'update with list value: {d}'\nassert d['dict'] == {'nested': True}, f'update with dict value: {d}'\n\n# === Multiple **kwargs updates ===\nd = {'a': 1}\nopts1 = {'b': 2}\nopts2 = {'c': 3}\nd.update(**opts1)\nd.update(**opts2)\nassert d == {'a': 1, 'b': 2, 'c': 3}, f'multiple updates with **kwargs: {d}'\n\n# === **kwargs overwriting existing keys ===\nd = {'a': 1, 'b': 2}\noverride_opts = {'b': 'new', 'c': 3}\nd.update(**override_opts)\nassert d == {'a': 1, 'b': 'new', 'c': 3}, f'update overwriting with **kwargs: {d}'\n\n# === Mixed *args and **kwargs with dict.update ===\n# dict.update can take a dict positionally AND **kwargs\nd = {'a': 1}\npos_update = {'b': 2}\nkw_update = {'c': 3}\nd.update(pos_update, **kw_update)\nassert d == {'a': 1, 'b': 2, 'c': 3}, f'update with pos and **kwargs: {d}'\n\n# === *args tuple unpacking combined with method ===\nd = {'a': 1}\nargs_tuple = ({'x': 10},)\nd.update(*args_tuple)\nassert d == {'a': 1, 'x': 10}, f'update with *args tuple: {d}'\n\n# === Combined *args and **kwargs ===\nd = {}\npos_dict = {'a': 1}\nkw_opts = {'b': 2}\nd.update(*[pos_dict], **kw_opts)\nassert d == {'a': 1, 'b': 2}, f'update with *args and **kwargs: {d}'\n\n# === Regular kwargs combined with **kwargs ===\n# This tests the code path where we have both explicit keyword args and **kwargs unpacking\nd = {}\nextra_opts = {'c': 3}\nd.update(a=1, b=2, **extra_opts)\nassert d == {'a': 1, 'b': 2, 'c': 3}, f'update with regular kwargs and **kwargs: {d}'\n\n# === Regular kwargs only (no **kwargs) with method call ===\nd = {}\nd.update(x=10, y=20)\nassert d == {'x': 10, 'y': 20}, f'update with regular kwargs only: {d}'\n\n# === Mixed positional, regular kwargs, and **kwargs ===\nd = {'existing': 0}\npos_update = {'a': 1}\nextra = {'d': 4}\nd.update(pos_update, b=2, c=3, **extra)\nassert d == {'existing': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4}, f'update with pos, kwargs, **kwargs: {d}'\n\n# === Empty **kwargs with regular kwargs ===\nd = {}\nempty_extra = {}\nd.update(x=1, **empty_extra)\nassert d == {'x': 1}, f'update with kwargs and empty **kwargs: {d}'\n\n# === **kwargs with different keys from regular kwargs ===\nd = {}\nextra = {'b': 'from_dict'}\nd.update(a='original', **extra)\nassert d == {'a': 'original', 'b': 'from_dict'}, f'update with different kwargs: {d}'\n\n# ============================================================\n# PEP 448 generalized method calls (multiple * or **)\n# ============================================================\n\n# === Multiple **kwargs in method call ===\nd = {}\nd.update(**{'a': 1}, **{'b': 2})\nassert d == {'a': 1, 'b': 2}, f'update with multiple **kwargs: {d}'\n\nd = {'x': 0}\nd.update(**{'a': 1}, **{'b': 2}, **{'c': 3})\nassert d == {'x': 0, 'a': 1, 'b': 2, 'c': 3}, f'update with three **kwargs: {d}'\n\n# Mixed named kwargs and multiple **kwargs\nd = {}\nd.update(a=1, **{'b': 2}, **{'c': 3})\nassert d == {'a': 1, 'b': 2, 'c': 3}, f'update named + multiple **kwargs: {d}'\n\n# === Positional args mixed with *unpack in method GeneralizedCall ===\n# insert(*[0], 1): positional 1 comes AFTER the *unpack → GeneralizedCall.\n# This exercises CallArg::Unpack (the *[0]) and CallArg::Value (the 1)\n# in the compile_method_call GeneralizedCall branch.\nmy_list = [2, 3]\nmy_list.insert(*[0], 1)\nassert my_list == [1, 2, 3], 'insert: star index then positional value'\n\nmy_list2 = ['a', 'b', 'c', 'd']\nmy_list2.insert(*[1], 'x')\nassert my_list2 == ['a', 'x', 'b', 'c', 'd'], 'insert: star index then positional string'\n\n# === *args + multiple **kwargs in method GeneralizedCall ===\n# d.update(*[...], **{...}, **{...}): two **unpacks → GeneralizedCall (not ArgsKargs).\n# The *unpack in args covers CallArg::Unpack; the two **unpacks mean has_kwargs=True,\n# covering the kwargs dict-builder block in the compile_method_call GeneralizedCall branch.\nd = {}\nd.update(*[{}], **{'a': 1}, **{'b': 2})\nassert d == {'a': 1, 'b': 2}, 'update: star args + two star-star kwargs'\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__unbound_local_func.py",
    "content": "# Test that accessing a variable before assignment in a function raises UnboundLocalError\n# (In function scope, Python pre-scans for assignments so it knows x is local)\ndef foo():\n    print(x)\n    x = 1\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__unbound_local_func.py\", line 8, in <module>\n    foo()\n    ~~~~~\n  File \"name_error__unbound_local_func.py\", line 4, in foo\n    print(x)\n          ~\nUnboundLocalError: cannot access local variable 'x' where it is not associated with a value\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__unbound_local_module.py",
    "content": "# Test that accessing a variable before assignment at module level raises NameError\n# (Unlike function scope, module level doesn't pre-scan for assignments)\nprint(x)\nx = 1\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__unbound_local_module.py\", line 3, in <module>\n    print(x)\n          ~\nNameError: name 'x' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__undefined_call_chained.py",
    "content": "x = aaa_func() + bbb_func()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__undefined_call_chained.py\", line 1, in <module>\n    x = aaa_func() + bbb_func()\n        ~~~~~~~~\nNameError: name 'aaa_func' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__undefined_call_in_expr.py",
    "content": "x = 1 + missing_func()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__undefined_call_in_expr.py\", line 1, in <module>\n    x = 1 + missing_func()\n            ~~~~~~~~~~~~\nNameError: name 'missing_func' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__undefined_call_in_function.py",
    "content": "def outer():\n    return missing_func()\n\n\nouter()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__undefined_call_in_function.py\", line 5, in <module>\n    outer()\n    ~~~~~~~\n  File \"name_error__undefined_call_in_function.py\", line 2, in outer\n    return missing_func()\n           ~~~~~~~~~~~~\nNameError: name 'missing_func' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__undefined_call_with_args.py",
    "content": "missing_func(1, 2, 3)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__undefined_call_with_args.py\", line 1, in <module>\n    missing_func(1, 2, 3)\n    ~~~~~~~~~~~~\nNameError: name 'missing_func' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/name_error__undefined_global.py",
    "content": "# Test that accessing an undefined global name raises NameError\nunknown_func()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"name_error__undefined_global.py\", line 2, in <module>\n    unknown_func()\n    ~~~~~~~~~~~~\nNameError: name 'unknown_func' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/namedtuple__missing_attr.py",
    "content": "# Test AttributeError message for missing attribute on named tuple\nimport sys\n\nsys.version_info.foobar\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"namedtuple__missing_attr.py\", line 4, in <module>\n    sys.version_info.foobar\nAttributeError: 'sys.version_info' object has no attribute 'foobar'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/namedtuple__ops.py",
    "content": "import sys\n\nvi = sys.version_info\n\n# === Equality: same object ===\nassert vi == vi, 'namedtuple equals itself'\n\n# === Equality: two references ===\nvi2 = sys.version_info\nassert vi == vi2, 'two refs to same namedtuple are equal'\n\n# === Equality: namedtuple == equivalent tuple ===\nt = (vi.major, vi.minor, vi.micro, vi.releaselevel, vi.serial)\nassert vi == t, 'namedtuple equals equivalent tuple'\nassert t == vi, 'equivalent tuple equals namedtuple'\n\n# === Inequality: wrong length ===\nassert vi != (3,), 'namedtuple not equal to wrong-length tuple'\nassert (3,) != vi, 'wrong-length tuple not equal to namedtuple'\n\n# === Inequality: different values ===\nassert vi != (0, 0, 0, 'final', 0), 'namedtuple not equal to different values'\n\n# === Inequality: non-tuple types ===\nassert vi != 42, 'namedtuple not equal to int'\nassert vi != 'hello', 'namedtuple not equal to str'\nassert vi != None, 'namedtuple not equal to None'\nassert vi != [3, 14], 'namedtuple not equal to list'\n\n# === repr ===\nr = repr(vi)\nassert r.startswith('sys.version_info(major='), f'namedtuple repr starts with type name, {r!r}'\nassert ', minor=' in r, f'namedtuple repr has minor field, {r!r}'\nassert r.endswith(')'), f'namedtuple repr ends with paren, {r!r}'\n"
  },
  {
    "path": "crates/monty/test_cases/nonlocal__error_module_level.py",
    "content": "# nonlocal at module level is a syntax error\nnonlocal x  # type: ignore\n# Raise=SyntaxError('nonlocal declaration not allowed at module level')\n"
  },
  {
    "path": "crates/monty/test_cases/nonlocal__ops.py",
    "content": "# === Basic nonlocal read/write ===\ndef read_outer():\n    x = 10\n\n    def inner():\n        return x  # reads from outer scope\n\n    return inner()\n\n\nassert read_outer() == 10, 'nonlocal read'\n\n\ndef write_outer():\n    x = 10\n\n    def inner():\n        nonlocal x\n        x = 20\n\n    inner()\n    return x\n\n\nassert write_outer() == 20, 'nonlocal write'\n\n\n# === Classic counter pattern ===\ndef make_counter():\n    count = 0\n\n    def increment():\n        nonlocal count\n        count = count + 1\n        return count\n\n    return increment\n\n\ncounter2 = make_counter()\nassert counter2() == 1, 'counter first call'\nassert counter2() == 2, 'counter second call'\nassert counter2() == 3, 'counter third call'\n\n\n# === Implicit capture (read without nonlocal) ===\ndef implicit_capture():\n    a = 10\n    b = 20\n\n    def inner():\n        return a + b  # reads both from outer\n\n    return inner()\n\n\nassert implicit_capture() == 30, 'implicit capture multiple vars'\n\n\n# === Pass-through nesting ===\ndef pass_through():\n    x = 1\n\n    def middle():\n        nonlocal x\n        x = x + 10\n\n        def inner():\n            nonlocal x\n            x = x + 100\n            return x\n\n        r1 = inner()  # returns 111\n        r2 = x  # x is now 111\n        return r1 + r2  # 222\n\n    return middle()\n\n\nassert pass_through() == 222, 'nonlocal pass through'\n\n\n# === Deep nesting (3 levels) ===\ndef deep_nesting():\n    x = 1\n\n    def level2():\n        nonlocal x\n        x = x + 10\n\n        def level3():\n            nonlocal x\n            x = x + 100\n            return x\n\n        return level3()\n\n    return level2()\n\n\nassert deep_nesting() == 111, 'deep nesting'\n\n\n# === Deep nesting (4 levels) ===\ndef deep_pass_through():\n    val = 1\n\n    def level1():\n        nonlocal val\n        val = val + 1\n\n        def level2():\n            nonlocal val\n            val = val + 10\n\n            def level3():\n                nonlocal val\n                val = val + 100\n                return val\n\n            return level3()\n\n        return level2()\n\n    result = level1()  # val: 1 -> 2 -> 12 -> 112\n    return (result, val)\n\n\nassert deep_pass_through() == (112, 112), 'deep pass through 4 levels'\n\n\n# === Multiple independent cells ===\ndef multiple_cells():\n    a = 1\n    b = 10\n    c = 100\n\n    def modify_a():\n        nonlocal a\n        a = a + 1\n        return a\n\n    def modify_b():\n        nonlocal b\n        b = b + 10\n        return b\n\n    def modify_c():\n        nonlocal c\n        c = c + 100\n        return c\n\n    def read_all():\n        return a + b + c\n\n    r1 = modify_b()  # b = 20\n    r2 = modify_a()  # a = 2\n    r3 = modify_c()  # c = 200\n    r4 = read_all()  # 2 + 20 + 200 = 222\n    return (r1, r2, r3, r4)\n\n\nassert multiple_cells() == (20, 2, 200, 222), 'multiple independent cells'\n\n\n# === Shared cell (getter/setter pattern) ===\ndef shared_cell():\n    x = 0\n\n    def getter():\n        return x\n\n    def setter(v):\n        nonlocal x\n        x = v\n\n    return (getter, setter)\n\n\npair = shared_cell()\ngetter = pair[0]\nsetter = pair[1]\nassert getter() == 0, 'shared cell initial'\nsetter(42)\nassert getter() == 42, 'shared cell after setter'\n\n\n# === Shared multiple vars ===\ndef shared_multiple_vars():\n    x = 0\n    y = 0\n\n    def add_to_x(n):\n        nonlocal x\n        x = x + n\n        return x\n\n    def add_to_y(n):\n        nonlocal y\n        y = y + n\n        return y\n\n    def swap():\n        nonlocal x, y\n        tmp = x\n        x = y\n        y = tmp\n        return (x, y)\n\n    def get_both():\n        return (x, y)\n\n    return (add_to_x, add_to_y, swap, get_both)\n\n\nops = shared_multiple_vars()\nadd_x = ops[0]\nadd_y = ops[1]\nswap = ops[2]\nget = ops[3]\nadd_x(5)  # x=5\nadd_y(10)  # y=10\nadd_x(3)  # x=8\nswap()  # x=10, y=8\nassert get() == (10, 8), 'shared multiple vars with swap'\n\n\n# === Local and captured ===\ndef local_and_captured():\n    x = 1\n\n    def inner():\n        nonlocal x\n        x = x + x\n        return x\n\n    before = x  # 1\n    middle = inner()  # 2\n    after = x  # 2\n    final = inner()  # 4\n    return (before, middle, after, final, x)\n\n\nassert local_and_captured() == (1, 2, 2, 4, 4), 'local and captured'\n\n\n# === Mixing global and nonlocal ===\ng1 = 100\n\n\ndef global_and_nonlocal():\n    x = 1\n\n    def inner():\n        global g1\n        nonlocal x\n        g1 = g1 + 1\n        x = x + 10\n        return g1 + x\n\n    return inner()\n\n\nassert global_and_nonlocal() == 112, 'global and nonlocal together'\n\n\n# === Closure with global and nonlocal ===\ng2 = 1000\n\n\ndef make_closure_global():\n    x = 1\n\n    def closure():\n        global g2\n        nonlocal x\n        result = g2 + x\n        g2 = g2 + 1\n        x = x + 10\n        return result\n\n    return closure\n\n\nc = make_closure_global()\nr1 = c()  # returns 1001\nr2 = c()  # returns 1012\nr3 = c()  # returns 1023\nassert (r1, r2, r3, g2) == (1001, 1012, 1023, 1003), 'closure with global and nonlocal'\n\n\n# === Closure creates closure ===\ndef outer_factory():\n    outer_val = 10\n\n    def inner_factory():\n        nonlocal outer_val\n        inner_val = outer_val\n\n        def innermost():\n            nonlocal inner_val\n            inner_val = inner_val + 1\n            return inner_val\n\n        outer_val = outer_val + 100\n        return innermost\n\n    return inner_factory\n\n\nfactory = outer_factory()\nclosure1 = factory()  # inner_val=10, outer_val->110\nclosure2 = factory()  # inner_val=110, outer_val->210\nr1 = closure1()  # 11\nr2 = closure1()  # 12\nr3 = closure2()  # 111\nr4 = closure1()  # 13\nassert (r1, r2, r3, r4) == (11, 12, 111, 13), 'closure creates closure'\n\n\n# === Augmented assignment with nonlocal ===\ndef augmented_assign():\n    x = 10\n\n    def inner():\n        nonlocal x\n        x += 5\n\n    inner()\n    return x\n\n\nassert augmented_assign() == 15, 'augmented assign nonlocal'\n\n\n# === Cell contains closure ===\ndef cell_contains_closure():\n    y = 100\n\n    def inner():\n        return y\n\n    x = inner  # x holds closure, x is also a cell var\n\n    def get_x():\n        nonlocal x\n        return x\n\n    f = get_x()\n    return f()\n\n\nassert cell_contains_closure() == 100, 'cell contains closure'\n"
  },
  {
    "path": "crates/monty/test_cases/os__environ.py",
    "content": "# call-external\n# Tests for os.environ property\n\nimport os\n\n# === os.environ property ===\n# os.environ returns a dict-like object\nenv = os.environ\n\n# === os.environ key access ===\nassert env['VIRTUAL_HOME'] == '/virtual/home', 'environ key access VIRTUAL_HOME'\nassert os.environ['VIRTUAL_HOME'] == '/virtual/home', 'environ key access VIRTUAL_HOME'\nassert os.environ['VIRTUAL_USER'] == 'testuser', 'environ key access VIRTUAL_USER'\nassert os.environ['VIRTUAL_EMPTY'] == '', 'environ key access VIRTUAL_EMPTY'\n\n# === os.environ get method ===\nassert env.get('VIRTUAL_HOME') == '/virtual/home', 'environ.get existing key'\nassert os.environ.get('VIRTUAL_HOME') == '/virtual/home', 'environ.get existing key'\nassert os.environ.get('VIRTUAL_USER') == 'testuser', 'environ.get existing user'\nassert os.environ.get('NONEXISTENT_VAR_12345') is None, 'environ.get missing returns None'\nassert os.environ.get('NONEXISTENT_VAR_12345', 'default') == 'default', 'environ.get with default'\n\n# === os.environ length ===\nassert len(env) == 3, 'environ has 3 virtual entries'\n\n# === os.environ membership test ===\nassert 'VIRTUAL_HOME' in env, 'VIRTUAL_HOME in environ'\nassert 'VIRTUAL_HOME' in os.environ, 'VIRTUAL_HOME in environ'\nassert 'VIRTUAL_USER' in env, 'VIRTUAL_USER in environ'\nassert 'NONEXISTENT_VAR_12345' not in env, 'nonexistent not in environ'\nassert 'NONEXISTENT_VAR_12345' not in os.environ, 'nonexistent not in environ'\n\n# === os.environ keys/values/items ===\nkeys = list(os.environ.keys())\nassert 'VIRTUAL_HOME' in keys, 'VIRTUAL_HOME in keys'\nassert 'VIRTUAL_USER' in keys, 'VIRTUAL_USER in keys'\n\nvalues = list(os.environ.values())\nassert '/virtual/home' in values, '/virtual/home in values'\nassert 'testuser' in values, 'testuser in values'\n"
  },
  {
    "path": "crates/monty/test_cases/os__getenv_key_list_error.py",
    "content": "# call-external\nimport os\n\nos.getenv([1, 2, 3])\n# Raise=TypeError('str expected, not list')\n"
  },
  {
    "path": "crates/monty/test_cases/os__getenv_key_type_error.py",
    "content": "# call-external\nimport os\n\nos.getenv(123)\n# Raise=TypeError('str expected, not int')\n"
  },
  {
    "path": "crates/monty/test_cases/parse_error__complex.py",
    "content": "# xfail=cpython\n1 + 2j\n# Raise=NotImplementedError('The monty syntax parser does not yet support complex constants')\n"
  },
  {
    "path": "crates/monty/test_cases/pathlib__import.py",
    "content": "import pathlib\n\n# Verify that pathlib.Path can be called as an attribute\np = pathlib.Path('a.txt')\nassert p.name == 'a.txt'\n\n# Verify that it still works when imported directly\nfrom pathlib import Path\n\np2 = Path('b.txt')\nassert p2.name == 'b.txt'\n"
  },
  {
    "path": "crates/monty/test_cases/pathlib__os.py",
    "content": "# call-external\nfrom pathlib import Path\n\n# === exists() ===\nassert Path('/virtual/file.txt').exists() == True, 'file exists'\nassert Path('/virtual/subdir').exists() == True, 'dir exists'\nassert Path('/virtual/subdir/deep').exists() == True, 'nested dir exists'\nassert Path('/nonexistent').exists() == False, 'nonexistent path'\nassert Path('/nonexistent/file.txt').exists() == False, 'nonexistent nested path'\n\n# === is_file() ===\nassert Path('/virtual/file.txt').is_file() == True, 'is_file true for file'\nassert Path('/virtual/subdir').is_file() == False, 'is_file false for dir'\nassert Path('/nonexistent').is_file() == False, 'is_file false for nonexistent'\n\n# === is_dir() ===\nassert Path('/virtual/subdir').is_dir() == True, 'is_dir true for dir'\nassert Path('/virtual/file.txt').is_dir() == False, 'is_dir false for file'\nassert Path('/nonexistent').is_dir() == False, 'is_dir false for nonexistent'\n\n# === is_symlink() ===\nassert Path('/virtual/file.txt').is_symlink() == False, 'is_symlink false (no symlinks in vfs)'\nassert Path('/nonexistent').is_symlink() == False, 'is_symlink false for nonexistent'\n\n# === read_text() ===\nassert Path('/virtual/file.txt').read_text() == 'hello world\\n', 'read_text basic'\nassert Path('/virtual/empty.txt').read_text() == '', 'read_text empty file'\nassert Path('/virtual/subdir/nested.txt').read_text() == 'nested content', 'read_text nested'\nassert Path('/virtual/subdir/deep/file.txt').read_text() == 'deep', 'read_text deep nested'\n\n# === read_bytes() ===\nassert Path('/virtual/data.bin').read_bytes() == b'\\x00\\x01\\x02\\x03', 'read_bytes binary'\nassert Path('/virtual/empty.txt').read_bytes() == b'', 'read_bytes empty'\nassert Path('/virtual/file.txt').read_bytes() == b'hello world\\n', 'read_bytes text file'\n\n# === stat() basic ===\nst = Path('/virtual/file.txt').stat()\nassert st.st_size == 12, 'stat size (len of \"hello world\\\\n\")'\n# 0o644 permissions + regular file type bits (0o100000)\nassert st.st_mode & 0o777 == 0o644, 'stat mode permissions'\n# Verify it's a regular file using raw mode bits\n# S_IFREG = 0o100000, so check that file type bits match\nassert st.st_mode & 0o170000 == 0o100000, 'stat is regular file'\n\n# === stat() directory ===\nst_dir = Path('/virtual/subdir').stat()\n# S_IFDIR = 0o040000, so check that file type bits match\nassert st_dir.st_mode & 0o170000 == 0o040000, 'stat is directory'\nassert st_dir.st_mode & 0o777 == 0o755, 'stat dir mode permissions'\n\n# === stat() index access ===\nst2 = Path('/virtual/file.txt').stat()\nassert st2[6] == 12, 'stat index access for st_size'\nassert st2[0] & 0o777 == 0o644, 'stat index access for st_mode'\n\n# === iterdir() ===\nentries = list(Path('/virtual').iterdir())\nassert len(entries) == 5, 'iterdir returns correct count'\n\n# iterdir() should return Path objects, not strings\nfirst_entry = entries[0]\nassert isinstance(first_entry, Path), f'iterdir should return Path objects, got {type(first_entry)}'\n\n# Path objects should have .name attribute\nnames = [e.name for e in entries]\nassert 'file.txt' in names, 'iterdir contains file.txt'\nassert 'subdir' in names, 'iterdir contains subdir'\nassert 'data.bin' in names, 'iterdir contains data.bin'\n\n# Path objects should have .parent attribute\nassert entries[0].parent == Path('/virtual'), 'iterdir entry parent is correct'\n\n# === iterdir() nested ===\nnested_entries = list(Path('/virtual/subdir').iterdir())\nassert len(nested_entries) == 2, 'iterdir nested count'\nnested_names = [e.name for e in nested_entries]\nassert 'nested.txt' in nested_names, 'iterdir nested contains nested.txt'\nassert 'deep' in nested_names, 'iterdir nested contains deep'\n\n# === iterdir() entries can be used for further operations ===\n# Find the nested.txt entry and read it\nfor entry in nested_entries:\n    if entry.name == 'nested.txt':\n        assert entry.read_text() == 'nested content', 'iterdir entry can be read'\n\n# === resolve() ===\np = Path('/virtual/file.txt').resolve()\nassert str(p) == '/virtual/file.txt', 'resolve absolute path unchanged'\n\n# === absolute() ===\np2 = Path('/virtual/subdir').absolute()\nassert str(p2) == '/virtual/subdir', 'absolute path unchanged'\n\n# === path concatenation with OS calls ===\nbase = Path('/virtual')\nfull = base / 'subdir' / 'nested.txt'\nassert full.read_text() == 'nested content', 'path concat then read'\nassert full.exists() == True, 'path concat then exists'\n\n# === write_text() ===\nPath('/virtual/new_file.txt').write_text('created by write_text')\nassert Path('/virtual/new_file.txt').read_text() == 'created by write_text', 'write_text creates file'\n# Overwrite existing file\nPath('/virtual/file.txt').write_text('overwritten')\nassert Path('/virtual/file.txt').read_text() == 'overwritten', 'write_text overwrites'\n\n# === write_bytes() ===\nPath('/virtual/binary.dat').write_bytes(b'\\xff\\xfe\\xfd')\nassert Path('/virtual/binary.dat').read_bytes() == b'\\xff\\xfe\\xfd', 'write_bytes creates file'\n\n# === mkdir() ===\nPath('/virtual/new_dir').mkdir()\nassert Path('/virtual/new_dir').is_dir() == True, 'mkdir creates directory'\n# mkdir with parents\nPath('/virtual/a/b/c').mkdir(parents=True)\nassert Path('/virtual/a/b/c').is_dir() == True, 'mkdir parents creates nested'\n# mkdir with exist_ok\nPath('/virtual/new_dir').mkdir(exist_ok=True)  # Should not raise\n\n# === unlink() ===\nPath('/virtual/to_delete.txt').write_text('delete me')\nassert Path('/virtual/to_delete.txt').exists() == True, 'file exists before unlink'\nPath('/virtual/to_delete.txt').unlink()\nassert Path('/virtual/to_delete.txt').exists() == False, 'unlink removes file'\n\n# === rmdir() ===\nPath('/virtual/empty_dir').mkdir()\nassert Path('/virtual/empty_dir').is_dir() == True, 'dir exists before rmdir'\nPath('/virtual/empty_dir').rmdir()\nassert Path('/virtual/empty_dir').exists() == False, 'rmdir removes directory'\n\n# === rename() ===\nPath('/virtual/old_name.txt').write_text('rename test')\nPath('/virtual/old_name.txt').rename(Path('/virtual/new_name.txt'))\nassert Path('/virtual/old_name.txt').exists() == False, 'rename removes old path'\nassert Path('/virtual/new_name.txt').read_text() == 'rename test', 'rename creates new path'\n"
  },
  {
    "path": "crates/monty/test_cases/pathlib__os_read_error.py",
    "content": "# call-external\nfrom pathlib import Path\n\nPath('/nonexistent').read_text()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"pathlib__os_read_error.py\", line 4, in <module>\n    Path('/nonexistent').read_text()\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nFileNotFoundError: [Errno 2] No such file or directory: '/nonexistent'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/pathlib__pure.py",
    "content": "# === Path constructor ===\nfrom pathlib import Path\n\np = Path('/usr/local/bin/python')\nassert str(p) == '/usr/local/bin/python', 'Path str should match input'\n\n# Constructor with multiple arguments\nassert str(Path('folder', 'file.txt')) == 'folder/file.txt', 'Path with two args joins'\nassert str(Path('/usr', 'local', 'bin')) == '/usr/local/bin', 'Path with three args joins'\nassert str(Path('start', '/absolute', 'end')) == '/absolute/end', 'absolute in middle replaces'\n\n# Constructor with no arguments\nassert str(Path()) == '.', 'Path() returns current dir'\n\n# === name property ===\nassert p.name == 'python', 'name should be final component'\nassert Path('/usr/local/bin/').name == 'bin', 'name should handle trailing slash'\nassert Path('/').name == '', 'root path should have empty name'\nassert Path('file.txt').name == 'file.txt', 'relative path name'\n\n# === parent property ===\nassert str(p.parent) == '/usr/local/bin', 'parent should remove last component'\nassert str(Path('/usr').parent) == '/', 'parent of first-level should be root'\nassert str(Path('/').parent) == '/', 'parent of root is root'\nassert str(Path('file.txt').parent) == '.', 'parent of relative without dir is .'\n\n# === stem property ===\nassert Path('/path/file.tar.gz').stem == 'file.tar', 'stem removes last extension'\nassert Path('/path/file.txt').stem == 'file', 'stem removes single extension'\nassert Path('/path/.bashrc').stem == '.bashrc', 'stem preserves hidden files'\nassert Path('/path/file').stem == 'file', 'stem without extension'\n\n# === suffix property ===\nassert Path('/path/file.tar.gz').suffix == '.gz', 'suffix is last extension'\nassert Path('/path/file.txt').suffix == '.txt', 'suffix with single extension'\nassert Path('/path/.bashrc').suffix == '', 'hidden file has no suffix'\nassert Path('/path/file').suffix == '', 'no extension means empty suffix'\n\n# === suffixes property ===\nassert Path('/path/file.tar.gz').suffixes == ['.tar', '.gz'], 'suffixes list'\nassert Path('/path/file.txt').suffixes == ['.txt'], 'single suffix as list'\nassert Path('/path/.bashrc').suffixes == [], 'hidden file has no suffixes'\n\n# === parts property ===\nassert Path('/usr/local/bin').parts == ('/', 'usr', 'local', 'bin'), 'absolute path parts'\nassert Path('usr/local').parts == ('usr', 'local'), 'relative path parts'\nassert Path('/').parts == ('/',), 'root path parts'\n\n# === is_absolute method ===\nassert Path('/usr/bin').is_absolute() == True, 'absolute path'\nassert Path('usr/bin').is_absolute() == False, 'relative path not absolute'\nassert Path('').is_absolute() == False, 'empty path not absolute'\n\n# === joinpath method ===\nassert str(Path('/usr').joinpath('local')) == '/usr/local', 'joinpath with one arg'\nassert str(Path('/usr').joinpath('local', 'bin')) == '/usr/local/bin', 'joinpath with two args'\nassert str(Path('/usr').joinpath('/etc')) == '/etc', 'joinpath with absolute replaces'\nassert str(Path('.').joinpath('file')) == 'file', 'joinpath from dot'\n\n# === with_name method ===\nassert str(Path('/path/file.txt').with_name('other.py')) == '/path/other.py', 'with_name replaces name'\nassert str(Path('file.txt').with_name('other.py')) == 'other.py', 'with_name on relative'\n\n# === with_suffix method ===\nassert str(Path('/path/file.txt').with_suffix('.py')) == '/path/file.py', 'with_suffix replaces'\nassert str(Path('/path/file.txt').with_suffix('')) == '/path/file', 'with_suffix removes'\nassert str(Path('/path/file').with_suffix('.txt')) == '/path/file.txt', 'with_suffix adds'\n\n# === / operator ===\nassert str(Path('/usr') / 'local') == '/usr/local', '/ operator joins'\nassert str(Path('/usr') / 'local' / 'bin') == '/usr/local/bin', '/ operator chains'\n\n# === as_posix method ===\nassert Path('/usr/bin').as_posix() == '/usr/bin', 'as_posix returns string'\n\n# === __fspath__ method (os.PathLike protocol) ===\nassert Path('/usr/bin').__fspath__() == '/usr/bin', '__fspath__ returns string'\n\n# === repr ===\nr = repr(Path('/usr/bin'))\nassert r == \"PosixPath('/usr/bin')\", f'repr should be PosixPath, got {r}'\n"
  },
  {
    "path": "crates/monty/test_cases/pyobject__cycle_dict_self.py",
    "content": "# Test that returning a cyclic dict doesn't crash (MontyObject cycle detection)\nd = {}\nd['self'] = d\nd\n# Return={'self': {...}}\n"
  },
  {
    "path": "crates/monty/test_cases/pyobject__cycle_list_dict.py",
    "content": "# Test composite cycle: list containing dict containing original list\nc = []\ne = {'list': c}\nc.append(e)\nc\n# Return=[{'list': [...]}]\n"
  },
  {
    "path": "crates/monty/test_cases/pyobject__cycle_list_self.py",
    "content": "# Test that returning a cyclic list doesn't crash (MontyObject cycle detection)\na = []\na.append(a)\na\n# Return=[[...]]\n"
  },
  {
    "path": "crates/monty/test_cases/pyobject__cycle_multiple_refs.py",
    "content": "# Test multiple references to the same cyclic object\nf = []\nf.append(f)\ng = [f, f]\ng\n# Return=[[[...]], [[...]]]\n"
  },
  {
    "path": "crates/monty/test_cases/range__error_no_args.py",
    "content": "range()\n# Raise=TypeError('range expected at least 1 argument, got 0')\n"
  },
  {
    "path": "crates/monty/test_cases/range__error_step_zero.py",
    "content": "range(0, 10, 0)\n# Raise=ValueError('range() arg 3 must not be zero')\n"
  },
  {
    "path": "crates/monty/test_cases/range__error_too_many_args.py",
    "content": "range(1, 2, 3, 4)\n# Raise=TypeError('range expected at most 3 arguments, got 4')\n"
  },
  {
    "path": "crates/monty/test_cases/range__getitem_index_error.py",
    "content": "r = range(5)\nr[10]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"range__getitem_index_error.py\", line 2, in <module>\n    r[10]\n    ~~~~~\nIndexError: range object index out of range\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/range__ops.py",
    "content": "# === range() with one argument (stop) ===\nassert list(range(0)) == [], 'range(0) is empty'\nassert list(range(1)) == [0], 'range(1) is [0]'\nassert list(range(5)) == [0, 1, 2, 3, 4], 'range(5) is [0, 1, 2, 3, 4]'\nassert list(range(-3)) == [], 'range negative stop is empty'\n\n# === range() with two arguments (start, stop) ===\nassert list(range(0, 3)) == [0, 1, 2], 'range(0, 3)'\nassert list(range(1, 5)) == [1, 2, 3, 4], 'range(1, 5)'\nassert list(range(5, 10)) == [5, 6, 7, 8, 9], 'range(5, 10)'\nassert list(range(3, 3)) == [], 'range equal start stop is empty'\nassert list(range(5, 3)) == [], 'range start > stop is empty'\nassert list(range(-5, -2)) == [-5, -4, -3], 'range negative to negative'\nassert list(range(-3, 2)) == [-3, -2, -1, 0, 1], 'range negative to positive'\n\n# === range() with three arguments (start, stop, step) ===\nassert list(range(0, 10, 2)) == [0, 2, 4, 6, 8], 'range step 2'\nassert list(range(1, 10, 3)) == [1, 4, 7], 'range step 3'\nassert list(range(0, 10, 5)) == [0, 5], 'range step 5'\nassert list(range(0, 10, 10)) == [0], 'range step equals diff'\nassert list(range(0, 10, 20)) == [0], 'range step > diff'\n\n# === range() with negative step ===\nassert list(range(10, 0, -1)) == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], 'range step -1'\nassert list(range(10, 0, -2)) == [10, 8, 6, 4, 2], 'range step -2'\nassert list(range(5, 0, -1)) == [5, 4, 3, 2, 1], 'range 5 to 0 step -1'\nassert list(range(0, 5, -1)) == [], 'range start < stop with negative step is empty'\nassert list(range(-1, -5, -1)) == [-1, -2, -3, -4], 'range negative with negative step'\n\n# === tuple(range()) conversions ===\nassert tuple(range(3)) == (0, 1, 2), 'tuple(range(3))'\nassert tuple(range(1, 4)) == (1, 2, 3), 'tuple(range(1, 4))'\nassert tuple(range(0, 6, 2)) == (0, 2, 4), 'tuple(range(0, 6, 2))'\n\n# === range in for loops ===\ntotal = 0\nfor i in range(5):\n    total = total + i\nassert total == 10, 'for loop with range(5)'\n\ntotal2 = 0\nfor i in range(1, 4):\n    total2 = total2 + i\nassert total2 == 6, 'for loop with range(1, 4)'\n\ntotal3 = 0\nfor i in range(0, 10, 2):\n    total3 = total3 + i\nassert total3 == 20, 'for loop with range step 2'\n\n# count down\ncountdown = []\nfor i in range(3, 0, -1):\n    countdown.append(i)\nassert countdown == [3, 2, 1], 'for loop countdown'\n\n# === range repr ===\nassert repr(range(5)) == 'range(0, 5)', 'repr range one arg'\nassert repr(range(1, 5)) == 'range(1, 5)', 'repr range two args'\nassert repr(range(1, 5, 2)) == 'range(1, 5, 2)', 'repr range three args'\nassert repr(range(0, 10, 1)) == 'range(0, 10)', 'repr range step 1 omitted'\nassert repr(range(5, 0, -1)) == 'range(5, 0, -1)', 'repr range negative step'\n\n# === range type ===\nassert type(range(5)) == range, 'type of range'\nassert type(range(1, 5)) == range, 'type of range two args'\nassert type(range(1, 5, 2)) == range, 'type of range three args'\n\n# === range equality ===\nassert range(5) == range(5), 'range equality same'\nassert range(0, 5) == range(5), 'range(0, 5) == range(5)'\nassert range(1, 5) == range(1, 5), 'range equality two args'\nassert range(1, 5, 2) == range(1, 5, 2), 'range equality three args'\nassert range(5) != range(6), 'range inequality'\nassert range(1, 5) != range(2, 5), 'range inequality start differs'\nassert range(1, 5, 1) != range(1, 5, 2), 'range inequality step differs'\n\n# === range bool (truthiness) ===\nassert bool(range(5)) == True, 'non-empty range is truthy'\nassert bool(range(1, 5)) == True, 'range(1, 5) is truthy'\nassert bool(range(0)) == False, 'empty range(0) is falsy'\nassert bool(range(5, 5)) == False, 'empty range equal start stop is falsy'\nassert bool(range(5, 0)) == False, 'empty range start > stop is falsy'\nassert bool(range(5, 0, -1)) == True, 'range countdown is truthy'\nassert bool(range(0, 5, -1)) == False, 'empty range wrong direction is falsy'\n\n# === range isinstance ===\nassert isinstance(range(5), range), 'isinstance range'\n\n# === len(range()) ===\nassert len(range(5)) == 5, 'len(range(5))'\nassert len(range(0)) == 0, 'len(range(0))'\nassert len(range(1, 5)) == 4, 'len(range(1, 5))'\nassert len(range(0, 10, 2)) == 5, 'len(range step 2)'\nassert len(range(10, 0, -1)) == 10, 'len(range negative step)'\nassert len(range(0, 10, 3)) == 4, 'len(range step 3)'\n\n# === range equality by sequence (not parameters) ===\nassert range(0, 3, 2) == range(0, 4, 2), 'ranges with same sequence [0,2] are equal'\nassert range(0, 5, 2) == range(0, 6, 2), 'range(0,5,2) == range(0,6,2) both [0,2,4]'\nassert range(5, 0, -2) == range(5, -1, -2), 'negative step same sequence'\nassert range(0) == range(0), 'empty ranges equal'\nassert range(5, 5) == range(10, 10), 'different empty ranges equal'\nassert range(0, 0) == range(5, 5), 'empty ranges with different params equal'\n\n# === Range indexing (getitem) ===\n# Basic indexing for range(stop)\nr = range(5)\nassert r[0] == 0, 'range(5)[0]'\nassert r[1] == 1, 'range(5)[1]'\nassert r[4] == 4, 'range(5)[4]'\n\n# Negative indexing\nassert r[-1] == 4, 'range(5)[-1]'\nassert r[-2] == 3, 'range(5)[-2]'\nassert r[-5] == 0, 'range(5)[-5]'\n\n# Range with start\nr = range(10, 15)\nassert r[0] == 10, 'range(10, 15)[0]'\nassert r[1] == 11, 'range(10, 15)[1]'\nassert r[4] == 14, 'range(10, 15)[4]'\nassert r[-1] == 14, 'range(10, 15)[-1]'\nassert r[-5] == 10, 'range(10, 15)[-5]'\n\n# Range with step\nr = range(0, 10, 2)\nassert r[0] == 0, 'range(0, 10, 2)[0]'\nassert r[1] == 2, 'range(0, 10, 2)[1]'\nassert r[2] == 4, 'range(0, 10, 2)[2]'\nassert r[3] == 6, 'range(0, 10, 2)[3]'\nassert r[4] == 8, 'range(0, 10, 2)[4]'\nassert r[-1] == 8, 'range(0, 10, 2)[-1]'\nassert r[-2] == 6, 'range(0, 10, 2)[-2]'\n\n# Range with step 3\nr = range(1, 10, 3)\nassert r[0] == 1, 'range(1, 10, 3)[0]'\nassert r[1] == 4, 'range(1, 10, 3)[1]'\nassert r[2] == 7, 'range(1, 10, 3)[2]'\nassert r[-1] == 7, 'range(1, 10, 3)[-1]'\n\n# Range with negative step\nr = range(10, 0, -1)\nassert r[0] == 10, 'range(10, 0, -1)[0]'\nassert r[1] == 9, 'range(10, 0, -1)[1]'\nassert r[9] == 1, 'range(10, 0, -1)[9]'\nassert r[-1] == 1, 'range(10, 0, -1)[-1]'\nassert r[-10] == 10, 'range(10, 0, -1)[-10]'\n\n# Range with negative step and larger step\nr = range(10, 0, -2)\nassert r[0] == 10, 'range(10, 0, -2)[0]'\nassert r[1] == 8, 'range(10, 0, -2)[1]'\nassert r[2] == 6, 'range(10, 0, -2)[2]'\nassert r[3] == 4, 'range(10, 0, -2)[3]'\nassert r[4] == 2, 'range(10, 0, -2)[4]'\nassert r[-1] == 2, 'range(10, 0, -2)[-1]'\n\n# Range starting from negative\nr = range(-5, 0)\nassert r[0] == -5, 'range(-5, 0)[0]'\nassert r[2] == -3, 'range(-5, 0)[2]'\nassert r[-1] == -1, 'range(-5, 0)[-1]'\n\n# Single element range\nr = range(42, 43)\nassert r[0] == 42, 'single element range[0]'\nassert r[-1] == 42, 'single element range[-1]'\n\n# Variable index\nr = range(100)\ni = 50\nassert r[i] == 50, 'range getitem with variable index'\n\n# Bool indices (True=1, False=0)\nr = range(10, 15)\nassert r[False] == 10, 'range getitem with False'\nassert r[True] == 11, 'range getitem with True'\n\n# === Range containment ('in' operator) ===\n# Basic containment\nassert 0 in range(5), '0 in range(5)'\nassert 4 in range(5), '4 in range(5)'\nassert 5 not in range(5), '5 not in range(5)'\nassert -1 not in range(5), '-1 not in range(5)'\n\n# Range with start\nassert 10 in range(10, 15), '10 in range(10, 15)'\nassert 14 in range(10, 15), '14 in range(10, 15)'\nassert 15 not in range(10, 15), '15 not in range(10, 15)'\nassert 9 not in range(10, 15), '9 not in range(10, 15)'\n\n# Range with step\nassert 0 in range(0, 10, 2), '0 in range(0, 10, 2)'\nassert 2 in range(0, 10, 2), '2 in range(0, 10, 2)'\nassert 8 in range(0, 10, 2), '8 in range(0, 10, 2)'\nassert 3 not in range(0, 10, 2), '3 not in range(0, 10, 2)'\nassert 10 not in range(0, 10, 2), '10 not in range(0, 10, 2)'\n\n# Range with negative step\nassert 10 in range(10, 0, -1), '10 in countdown'\nassert 1 in range(10, 0, -1), '1 in countdown'\nassert 0 not in range(10, 0, -1), '0 not in countdown'\nassert 11 not in range(10, 0, -1), '11 not in countdown'\n\n# Negative step with step > 1\nassert 10 in range(10, 0, -2), '10 in range(10, 0, -2)'\nassert 8 in range(10, 0, -2), '8 in range(10, 0, -2)'\nassert 9 not in range(10, 0, -2), '9 not in range(10, 0, -2)'\n\n# Negative ranges\nassert -3 in range(-5, 0), '-3 in range(-5, 0)'\nassert 0 not in range(-5, 0), '0 not in range(-5, 0)'\n\n# Empty ranges\nassert 5 not in range(0), '5 not in empty range'\nassert 0 not in range(5, 5), '0 not in empty equal range'\n\n# Non-int types return False (no TypeError)\nassert 'a' not in range(5), 'string not in range'\n\n# Float containment (floats equal to integers are contained)\nassert 3.0 in range(5), '3.0 in range(5)'\nassert 0.0 in range(5), '0.0 in range(5)'\nassert 4.0 in range(5), '4.0 in range(5)'\nassert 3.5 not in range(5), '3.5 not in range(5)'\nassert 5.0 not in range(5), '5.0 not in range(5)'\nassert 2.0 in range(0, 10, 2), '2.0 in even range'\nassert 3.0 not in range(0, 10, 2), '3.0 not in even range'\nassert -1.0 not in range(5), '-1.0 not in range(5)'\n\n# Bool as container element (True=1, False=0 for comparison)\nassert True in range(5), 'True in range(5)'\nassert False in range(5), 'False in range(5)'\nassert True not in range(0), 'True not in empty range'\n"
  },
  {
    "path": "crates/monty/test_cases/re__basic.py",
    "content": "# Tests for the re (regular expression) module - basic functionality\n\nimport re\n\n# === Constant ===\nassert re.NOFLAG == 0, 're.NOFLAG == 0'\nassert re.I == re.IGNORECASE == 2, 're.I == re.IGNORECASE == 2'\nassert re.M == re.MULTILINE == 8, 're.M == re.MULTILINE == 8'\nassert re.S == re.DOTALL == 16, 're.S == re.DOTALL == 16'\n\n# === re.search() basic ===\nm = re.search('hello', 'say hello world')\nassert m is not None, 're.search finds a match'\nassert m.group() == 'hello', 're.search group(0) returns matched text'\nassert m.group(0) == 'hello', 're.search group(0) explicit returns matched text'\nassert m.start() == 4, 're.search start() returns start position'\nassert m.end() == 9, 're.search end() returns end position'\nassert m.span() == (4, 9), 're.search span() returns (start, end) tuple'\n\n# === re.search() with no match ===\nm = re.search('xyz', 'hello world')\nassert m is None, 're.search returns None when no match'\n\n# === re.search() with error ===\ntry:\n    re.search('(', 'test')\n    assert False, 're.search with invalid pattern should raise error'\nexcept re.PatternError as e:\n    # The error message may vary based on the regex engine, but it should not be empty\n    assert len(str(e)) > 0, 're.search with invalid pattern raises PatternError with message'\n\n# === re.match() ===\nm = re.match('hello', 'hello world')\nassert m is not None, 're.match matches at start'\nassert m.group() == 'hello', 're.match group returns matched text'\n\nm = re.match('world', 'hello world')\nassert m is None, 're.match does not match in the middle'\n\n# === re.fullmatch() ===\nm = re.fullmatch('hello', 'hello')\nassert m is not None, 're.fullmatch matches exact string'\nassert m.group() == 'hello', 're.fullmatch group returns full match'\n\nm = re.fullmatch('hello', 'hello world')\nassert m is None, 're.fullmatch does not match partial string'\n\n# === re.findall() with no groups ===\nresult = re.findall(r'\\d+', 'a1 b22 c333')\nassert result == ['1', '22', '333'], 'findall without groups returns list of matches'\n\n# === re.findall() with no match ===\nresult = re.findall(r'\\d+', 'no numbers')\nassert result == [], 'findall with no match returns empty list'\n\n# === re.sub() ===\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3')\nassert result == 'aX bX cX', 're.sub replaces all matches'\n\n# === re.sub() with count ===\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', 1)\nassert result == 'aX b2 c3', 're.sub with count=1 replaces only first'\n\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', 2)\nassert result == 'aX bX c3', 're.sub with count=2 replaces first two'\n\n# === re.compile() ===\npattern = re.compile(r'\\d+')\nm = pattern.search('abc 123 def')\nassert m is not None, 'compiled pattern search finds match'\nassert m.group() == '123', 'compiled pattern match returns correct group'\n\nm = pattern.match('123 abc')\nassert m is not None, 'compiled pattern match at start'\nassert m.group() == '123', 'compiled pattern match group'\n\nm = pattern.match('abc 123')\nassert m is None, 'compiled pattern match does not match in middle'\n\n# === compiled pattern fullmatch ===\npattern = re.compile(r'\\d+')\nm = pattern.fullmatch('123')\nassert m is not None, 'compiled pattern fullmatch on exact string'\nassert m.group() == '123', 'compiled pattern fullmatch group'\n\nm = pattern.fullmatch('123abc')\nassert m is None, 'compiled pattern fullmatch rejects partial match'\n\n# === compiled pattern findall ===\npattern = re.compile(r'\\d+')\nresult = pattern.findall('a1 b2 c3')\nassert result == ['1', '2', '3'], 'compiled pattern findall'\n\n# === compiled pattern sub ===\npattern = re.compile(r'\\d+')\nresult = pattern.sub('X', 'a1 b2 c3')\nassert result == 'aX bX cX', 'compiled pattern sub'\n\nresult = pattern.sub('X', 'a1 b2 c3', 1)\nassert result == 'aX b2 c3', 'compiled pattern sub with count'\n\n# === Flags: IGNORECASE ===\npattern = re.compile(r'hello', re.IGNORECASE)\nm = pattern.search('Hello World')\nassert m is not None, 'IGNORECASE flag works'\nassert m.group() == 'Hello', 'IGNORECASE matches case-insensitively'\n\n# === Flags: DOTALL ===\npattern = re.compile(r'a.b', re.DOTALL)\nm = pattern.search('a\\nb')\nassert m is not None, 'DOTALL flag allows dot to match newline'\nassert m.group() == 'a\\nb', 'DOTALL matches newline with dot'\n\n# === Flags: MULTILINE ===\npattern = re.compile(r'^\\w+', re.MULTILINE)\nresult = pattern.findall('hello\\nworld')\nassert result == ['hello', 'world'], 'MULTILINE allows ^ to match at line boundaries'\n\n# === Pattern attributes ===\npattern = re.compile(r'\\d+', re.IGNORECASE)\nassert pattern.pattern == r'\\d+', '.pattern returns the pattern string'\n# CPython flags include re.UNICODE (32) by default, so we check flags & 2 instead\nassert pattern.flags & re.IGNORECASE, '.flags includes IGNORECASE'\n\n# === Pattern repr ===\np = re.compile(r'\\d+')\nassert repr(p) == r\"re.compile('\\\\d+')\", 'Pattern repr without flags'\n\np = re.compile(r'\\d+', re.IGNORECASE)\nassert repr(p) == r\"re.compile('\\\\d+', re.IGNORECASE)\", 'Pattern repr with IGNORECASE'\n\n# === Flag constants ===\nassert re.IGNORECASE == 2, 'IGNORECASE flag value'\nassert re.MULTILINE == 8, 'MULTILINE flag value'\nassert re.DOTALL == 16, 'DOTALL flag value'\n\n# === Combined flags ===\npattern = re.compile(r'^hello', re.IGNORECASE | re.MULTILINE)\nresult = pattern.findall('Hello\\nhello\\nHELLO')\nassert result == ['Hello', 'hello', 'HELLO'], 'Combined IGNORECASE | MULTILINE flags'\n\n# === More MULTILINE tests ===\n# Without MULTILINE, ^ matches only start of string\npattern = re.compile(r'^\\w+')\nresult = pattern.findall('line1\\nline2\\nline3')\nassert result == ['line1'], 'Without MULTILINE, ^ matches only start of string'\n\n# With MULTILINE, ^ matches each line start\npattern = re.compile(r'^\\w+', re.MULTILINE)\nresult = pattern.findall('line1\\nline2\\nline3')\nassert result == ['line1', 'line2', 'line3'], 'With MULTILINE, ^ matches each line start'\n\n# Without MULTILINE, $ matches only end of string\npattern = re.compile(r'\\w+$')\nresult = pattern.findall('line1\\nline2\\nline3')\nassert result == ['line3'], 'Without MULTILINE, $ matches only end of string'\n\n# With MULTILINE, $ matches each line end\npattern = re.compile(r'\\w+$', re.MULTILINE)\nresult = pattern.findall('line1\\nline2\\nline3')\nassert result == ['line1', 'line2', 'line3'], 'With MULTILINE, $ matches each line end'\n\n# === More DOTALL tests ===\n# Without DOTALL, . does not match newline\npattern = re.compile(r'a.b')\nm = pattern.search('a\\nb')\nassert m is None, 'Without DOTALL, . does not match newline'\n\n# With DOTALL, . matches newline\npattern = re.compile(r'a.b', re.DOTALL)\nm = pattern.search('a\\nb')\nassert m is not None, 'With DOTALL, . matches newline'\nassert m.group() == 'a\\nb', 'DOTALL allows . to match newline'\n\n# DOTALL with multiple newlines\npattern = re.compile(r'start.*end', re.DOTALL)\nm = pattern.search('start\\nline1\\nline2\\nend')\nassert m is not None, 'DOTALL .* matches multiple newlines'\nassert m.group() == 'start\\nline1\\nline2\\nend', 'DOTALL .* captures everything including newlines'\n\n# === Pattern repr with multiple flags (I, M, D order) ===\np = re.compile(r'test', re.IGNORECASE)\nassert repr(p) == r\"re.compile('test', re.IGNORECASE)\", 'Pattern repr with I flag'\n\np = re.compile(r'test', re.MULTILINE)\nassert repr(p) == r\"re.compile('test', re.MULTILINE)\", 'Pattern repr with M flag'\n\np = re.compile(r'test', re.DOTALL)\nassert repr(p) == r\"re.compile('test', re.DOTALL)\", 'Pattern repr with D flag'\n\np = re.compile(r'test', re.IGNORECASE | re.MULTILINE)\nassert repr(p) == r\"re.compile('test', re.IGNORECASE|re.MULTILINE)\", 'Pattern repr with I|M flags'\n\np = re.compile(r'test', re.IGNORECASE | re.DOTALL)\nassert repr(p) == r\"re.compile('test', re.IGNORECASE|re.DOTALL)\", 'Pattern repr with I|D flags'\n\np = re.compile(r'test', re.MULTILINE | re.DOTALL)\nassert repr(p) == r\"re.compile('test', re.MULTILINE|re.DOTALL)\", 'Pattern repr with M|D flags'\n\np = re.compile(r'test', re.IGNORECASE | re.MULTILINE | re.DOTALL)\nassert repr(p) == r\"re.compile('test', re.IGNORECASE|re.MULTILINE|re.DOTALL)\", 'Pattern repr with I|M|D flags'\n\n# === Combined IGNORECASE and DOTALL ===\npattern = re.compile(r'Hello.*World', re.IGNORECASE | re.DOTALL)\nm = pattern.search('HELLO\\nmiddle\\nWORLD')\nassert m is not None, 'Combined IGNORECASE|DOTALL finds match'\nassert m.group() == 'HELLO\\nmiddle\\nWORLD', 'IGNORECASE|DOTALL matches case-insensitively across newlines'\n\n# === Combined MULTILINE and DOTALL ===\npattern = re.compile(r'^a.*b$', re.MULTILINE | re.DOTALL)\nresult = pattern.findall('a\\nb\\nc\\nb')\nassert result == ['a\\nb\\nc\\nb'], 'Combined MULTILINE|DOTALL with ^ and $ and .'\n\n# === All three flags combined ===\npattern = re.compile(r'^Hello.*World$', re.IGNORECASE | re.MULTILINE | re.DOTALL)\nm = pattern.search('first\\nHELLO\\nsome\\nlines\\nWORLD\\nlast')\nassert m is not None, 'All three flags combined finds match'\nassert m.group() == 'HELLO\\nsome\\nlines\\nWORLD', 'I|M|D flags work together'\n\n# === Empty pattern ===\nm = re.search(r'', 'abc')\nassert m is not None, 'search with empty pattern finds match'\nassert m.start() == 0 and m.end() == 0, 'empty pattern matches at start of string'\n\n# === Zero-length matches ===\nm = re.search(r'a*', 'bc')\nassert m is not None, 'search with zero-length match finds match'\nassert m.group() == '', 'zero-length match returns empty string'\n\n# === Object identity of compiled patterns ===\np1 = re.compile(r'\\d+')\np2 = re.compile(r'\\d+')\nassert p1 == p2, 'separately compiled patterns with same pattern are equal'\nmatch1 = p1.search('123')\nmatch2 = p2.search('123')\nassert match1 != match2, 'matches from different pattern objects are distinct'\n\n# === re.sub() error: missing pattern ===\ntry:\n    re.sub()\n    assert False, 're.sub() with no args should raise TypeError'\nexcept TypeError as e:\n    assert 'pattern' in str(e).lower(), 're.sub missing pattern error mentions pattern'\n\n# === re.sub() error: missing repl ===\ntry:\n    re.sub(r'\\d+')\n    assert False, 're.sub(pattern) should raise TypeError'\nexcept TypeError as e:\n    assert 'repl' in str(e).lower(), 're.sub missing repl error mentions repl'\n\n# === re.sub() error: missing string ===\ntry:\n    re.sub(r'\\d+', 'X')\n    assert False, 're.sub(pattern, repl) should raise TypeError'\nexcept TypeError as e:\n    assert 'string' in str(e).lower(), 're.sub missing string error mentions string'\n\n# === re.sub() error: count is not an integer ===\ntry:\n    re.sub(r'\\d+', 'X', 'a1b2', 1.5)\n    assert False, 're.sub with float count should raise TypeError'\nexcept TypeError as e:\n    assert \"'float' object cannot be interpreted as an integer\" in str(e), 're.sub float count error'\n\ntry:\n    re.sub(r'\\d+', 'X', 'a1b2', 'one')\n    assert False, 're.sub with string count should raise TypeError'\nexcept TypeError as e:\n    assert \"'str' object cannot be interpreted as an integer\" in str(e), 're.sub string count error'\n\n# === Pattern.sub() error: missing repl ===\npattern = re.compile(r'\\d+')\ntry:\n    pattern.sub()\n    assert False, 'Pattern.sub() with no args should raise TypeError'\nexcept TypeError as e:\n    assert 'repl' in str(e).lower(), 'Pattern.sub missing repl error mentions repl'\n\n# === Pattern.sub() error: missing string ===\ntry:\n    pattern.sub('X')\n    assert False, 'Pattern.sub(repl) should raise TypeError'\nexcept TypeError as e:\n    assert 'string' in str(e).lower(), 'Pattern.sub missing string error mentions string'\n\n# === re.sub() with count=0 (replace all) ===\nresult = re.sub(r'\\d', 'X', '1a2b3c', 0)\nassert result == 'XaXbXc', 're.sub with count=0 replaces all'\n\n# === re.sub() empty replacement ===\nresult = re.sub(r'\\d+', '', 'a1 b2 c3')\nassert result == 'a b c', 're.sub with empty replacement removes matches'\n\n# === Pattern.sub() edge case: empty match ===\npattern = re.compile(r'a*')\nresult = pattern.sub('X', 'bac')\n# Note: this might be a zero-width match behavior that's different\nassert 'X' in result, 'Pattern.sub handles zero-width matches'\n\n# === re.compile() error: invalid pattern ===\ntry:\n    re.compile('(unclosed')\n    assert False, 're.compile with invalid pattern should raise PatternError'\nexcept re.PatternError as e:\n    assert len(str(e)) > 0, 're.compile invalid pattern raises PatternError'\n\n# === re.search() error: pattern is not a string ===\ntry:\n    re.search(123, 'hello')\n    assert False, 're.search with int pattern should raise TypeError'\nexcept TypeError as e:\n    assert 'string' in str(e).lower(), 're.search non-string pattern error'\n\n# === re.search() error: string is not a string ===\ntry:\n    re.search(r'\\d+', 123)\n    assert False, 're.search with int string should raise TypeError'\nexcept TypeError as e:\n    assert 'string' in str(e).lower(), 're.search non-string string error'\n\n# === re.match() error: pattern is not a string ===\ntry:\n    re.match(None, 'hello')\n    assert False, 're.match with None pattern should raise TypeError'\nexcept TypeError as e:\n    assert 'string' in str(e).lower(), 're.match None pattern error'\n\n# === re.fullmatch() error: string is not a string ===\ntry:\n    re.fullmatch(r'\\d+', None)\n    assert False, 're.fullmatch with None string should raise TypeError'\nexcept TypeError as e:\n    assert 'string' in str(e).lower(), 're.fullmatch None string error'\n\n# === Object basic ===\nassert bool(re.compile(r'\\d+'))\nassert bool(re.search(r'\\w+', 'hello'))\nassert isinstance(re.compile(r'\\d+'), re.Pattern), 're.compile returns re.Pattern instance'\nassert isinstance(re.search(r'\\w+', 'hello'), re.Match), 're.search returns re.Match instance'\nassert str(type(re.compile(r'\\d+'))) == \"<class 're.Pattern'>\", 'type of compiled pattern is re.Pattern'\nassert str(type(re.search(r'\\w+', 'hello'))) == \"<class 're.Match'>\", 'type of search match is re.Match'\n\n# === fullmatch with alternation ===\n# fullmatch must try all alternatives to find a full-string match,\n# not just pick the first alternative that matches somewhere\nm = re.fullmatch('a|ab', 'ab')\nassert m is not None, 'fullmatch with alternation finds full match'\nassert m.group() == 'ab', 'fullmatch alternation matches full-string alternative'\n\nm = re.fullmatch('ab|a', 'ab')\nassert m is not None, 'fullmatch when full-match alternative is first'\nassert m.group() == 'ab', 'fullmatch returns correct match when first alt matches'\n\nm = re.fullmatch('cat|category', 'category')\nassert m is not None, 'fullmatch alternation picks full-string alternative'\nassert m.group() == 'category', 'fullmatch alternation returns correct match'\n\nm = re.fullmatch('x|ab|a', 'ab')\nassert m is not None, 'fullmatch with three alternatives'\nassert m.group() == 'ab', 'fullmatch picks correct alternative from three'\n\n# compiled pattern fullmatch with alternation\np = re.compile('a|ab')\nm = p.fullmatch('ab')\nassert m is not None, 'compiled fullmatch with alternation finds full match'\nassert m.group() == 'ab', 'compiled fullmatch alternation matches correctly'\n\n# fullmatch with alternation and groups\nm = re.fullmatch('(a)|(ab)', 'ab')\nassert m is not None, 'fullmatch alternation with groups'\nassert m.group(0) == 'ab', 'fullmatch alternation groups: group(0) is full match'\nassert m.group(1) is None, 'fullmatch alternation groups: group(1) did not match'\nassert m.group(2) == 'ab', 'fullmatch alternation groups: group(2) matched'\n\n# fullmatch with quantifiers\nm = re.fullmatch('a+|b+', 'aaa')\nassert m is not None, 'fullmatch a+|b+ on aaa'\nassert m.group() == 'aaa', 'fullmatch a+|b+ returns full match'\n\n# fullmatch with .* (greedy)\nm = re.fullmatch('.*', 'anything')\nassert m is not None, 'fullmatch .* matches anything'\nassert m.group() == 'anything', 'fullmatch .* returns full string'\n\n# fullmatch on empty string with empty pattern\nm = re.fullmatch('', '')\nassert m is not None, 'fullmatch empty pattern on empty string'\nassert m.group() == '', 'fullmatch empty returns empty'\n\n# fullmatch should not match partial strings even with alternation\nm = re.fullmatch('a|ab', 'abc')\nassert m is None, 'fullmatch rejects when no alternative spans full string'\n\n# fullmatch with MULTILINE should still require full-string match\np = re.compile('hello', re.MULTILINE)\nm = p.fullmatch('hello')\nassert m is not None, 'fullmatch MULTILINE on single line'\nassert m.group() == 'hello', 'fullmatch MULTILINE returns correct match'\n\nm = p.fullmatch('hello\\nworld')\nassert m is None, 'fullmatch MULTILINE rejects multi-line input'\n\n# fullmatch with alternation and flags combined\np = re.compile('(a+)|(b+)', re.MULTILINE)\nm = p.fullmatch('bbb')\nassert m is not None, 'fullmatch groups with MULTILINE flag'\nassert m.group(0) == 'bbb', 'fullmatch groups MULTILINE: group(0) correct'\nassert m.group(1) is None, 'fullmatch groups MULTILINE: group(1) did not match'\nassert m.group(2) == 'bbb', 'fullmatch groups MULTILINE: group(2) matched'\n\n# === Literal $ in replacement ===\nresult = re.sub(r'\\d+', '$', 'a1b2')\nassert result == 'a$b$', 'literal $ in replacement is preserved'\n\nresult = re.sub(r'\\d+', '$1', 'a1b2')\nassert result == 'a$1b$1', 'literal $1 in replacement is preserved (not backreference)'\n\nresult = re.sub(r'\\d+', '$$', 'a1b2')\nassert result == 'a$$b$$', 'literal $$ in replacement is preserved'\n\n# compiled pattern with $ in replacement\np = re.compile(r'\\d+')\nresult = p.sub('$', 'a1b2')\nassert result == 'a$b$', 'compiled pattern: literal $ in replacement is preserved'\n\nresult = re.sub(r'\\d+', '$$$', 'a1b2')\nassert result == 'a$$$b$$$', 'triple $ in replacement preserved'\n\n# plain replacement with no special chars\nresult = re.sub(r'\\d+', 'NUM', 'a1 b2')\nassert result == 'aNUM bNUM', 'plain replacement without special chars'\n\n# === Negative count in re.sub ===\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', -1)\nassert result == 'a1 b2 c3', 're.sub with negative count returns string unchanged'\n\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', -100)\nassert result == 'a1 b2 c3', 're.sub with large negative count returns string unchanged'\n\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', -999)\nassert result == 'a1 b2 c3', 're.sub with very large negative count returns string unchanged'\n\n# compiled pattern with negative count\np = re.compile(r'\\d+')\nresult = p.sub('X', 'a1 b2 c3', -1)\nassert result == 'a1 b2 c3', 'compiled pattern: negative count returns string unchanged'\n\nresult = p.sub('X', 'a1 b2 c3', -100)\nassert result == 'a1 b2 c3', 'compiled pattern: large negative count returns string unchanged'\n\n# negative count with empty string\nresult = re.sub(r'\\d+', 'X', '', -1)\nassert result == '', 're.sub negative count on empty string'\n\n# === re.sub with count boundary values ===\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', 0)\nassert result == 'aX bX cX', 're.sub count=0 replaces all (explicit)'\n\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', 1)\nassert result == 'aX b2 c3', 're.sub count=1 replaces first only'\n\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', 3)\nassert result == 'aX bX cX', 're.sub count=3 replaces all three'\n\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', 100)\nassert result == 'aX bX cX', 're.sub count exceeding matches replaces all'\n\n# === Pattern.sub() error: too many arguments ===\np = re.compile(r'\\d+')\ntry:\n    p.sub('X', 'a1b2', 0, 'extra')\n    assert False, 'Pattern.sub with 4 args should raise TypeError'\nexcept TypeError as e:\n    assert 'at most 3' in str(e), 'Pattern.sub too many args error'\n\n# === Flags on module-level functions ===\n# re.search with flags\nm = re.search(r'hello', 'HELLO WORLD', re.IGNORECASE)\nassert m is not None, 're.search with IGNORECASE flag'\nassert m.group() == 'HELLO', 're.search IGNORECASE matches case-insensitively'\n\nm = re.search(r'hello', 'HELLO WORLD')\nassert m is None, 're.search without flags is case-sensitive'\n\n# re.match with flags\nm = re.match(r'hello', 'HELLO WORLD', re.IGNORECASE)\nassert m is not None, 're.match with IGNORECASE flag'\nassert m.group() == 'HELLO', 're.match IGNORECASE matches case-insensitively'\n\n# re.fullmatch with flags\nm = re.fullmatch(r'hello', 'HELLO', re.IGNORECASE)\nassert m is not None, 're.fullmatch with IGNORECASE flag'\nassert m.group() == 'HELLO', 're.fullmatch IGNORECASE matches case-insensitively'\n\n# re.findall with flags\nresult = re.findall(r'hello', 'Hello HELLO hello', re.IGNORECASE)\nassert result == ['Hello', 'HELLO', 'hello'], 're.findall with IGNORECASE flag'\n\n# re.sub with flags (5th positional arg)\nresult = re.sub(r'hello', 'X', 'Hello HELLO hello', 0, re.IGNORECASE)\nassert result == 'X X X', 're.sub with flags as 5th arg'\n\n# re.search with DOTALL flag\nm = re.search(r'a.b', 'a\\nb', re.DOTALL)\nassert m is not None, 're.search with DOTALL flag'\nassert m.group() == 'a\\nb', 're.search DOTALL matches across newlines'\n\n# re.findall with MULTILINE\nresult = re.findall(r'^\\w+', 'hello\\nworld\\nfoo', re.MULTILINE)\nassert result == ['hello', 'world', 'foo'], 're.findall with MULTILINE flag'\n\n# re.search with combined flags\nm = re.search(r'hello.*world', 'HELLO\\nWORLD', re.IGNORECASE | re.DOTALL)\nassert m is not None, 're.search with IGNORECASE | DOTALL'\nassert m.group() == 'HELLO\\nWORLD', 're.search combined flags work'\n\n# === re.ASCII flag ===\nassert re.ASCII == 256, 're.ASCII flag value'\nassert re.A == re.ASCII, 're.A is alias for re.ASCII'\n\n# re.ASCII flag is accepted (doesn't error)\np = re.compile(r'\\w+', re.ASCII)\nm = p.search('cafe')\nassert m is not None, 'ASCII mode matches ASCII word chars'\nassert m.group() == 'cafe', 'ASCII mode returns correct match'\n\n# re.ASCII can be combined with other flags\np = re.compile(r'hello', re.ASCII | re.IGNORECASE)\nm = p.search('HELLO')\nassert m is not None, 'ASCII | IGNORECASE combined'\nassert m.group() == 'HELLO', 'ASCII | IGNORECASE matches correctly'\n\n# Pattern repr with re.ASCII flag\np = re.compile(r'\\w+', re.ASCII)\nassert repr(p) == r\"re.compile('\\\\w+', re.ASCII)\", 'Pattern repr with ASCII flag'\n\np = re.compile(r'\\w+', re.ASCII | re.IGNORECASE)\nassert repr(p) == r\"re.compile('\\\\w+', re.IGNORECASE|re.ASCII)\", 'Pattern repr with ASCII|IGNORECASE flags'\n\n# re.ASCII on module-level functions\nm = re.search(r'\\w+', 'cafe', re.ASCII)\nassert m is not None, 're.search with re.ASCII flag'\nassert m.group() == 'cafe', 're.search re.ASCII returns correct match'\n\nm = re.match(r'\\w+', 'cafe', re.A)\nassert m is not None, 're.match with re.A alias'\nassert m.group() == 'cafe', 're.match re.A returns correct match'\n\nm = re.fullmatch(r'\\w+', 'cafe', re.ASCII)\nassert m is not None, 're.fullmatch with re.ASCII flag'\nassert m.group() == 'cafe', 're.fullmatch re.ASCII returns correct match'\n\nresult = re.findall(r'\\w+', 'a b c', re.ASCII)\nassert result == ['a', 'b', 'c'], 're.findall with re.ASCII flag'\n\n# === match with alternation (anchored) ===\n# re.match('b|ab', 'ab') must try alternation at position 0\nm = re.match(r'b|ab', 'ab')\nassert m is not None, 're.match with alternation at start'\nassert m.group() == 'ab', 're.match alternation: second alt matches at pos 0'\n\n# re.match with alternation: first alt doesn't start at pos 0\nm = re.match(r'world|hello', 'hello world')\nassert m is not None, 're.match alternation: finds match starting at pos 0'\nassert m.group() == 'hello', 're.match alternation: correct alternative matches'\n\n# compiled pattern match with alternation\np = re.compile(r'b|ab')\nm = p.match('ab')\nassert m is not None, 'Pattern.match with alternation'\nassert m.group() == 'ab', 'Pattern.match alternation: second alt matches at pos 0'\n\n# match with alternation where shorter alt matches at pos 0\nm = re.match(r'a|ab', 'ab')\nassert m is not None, 're.match alternation: shorter alt at pos 0'\nassert m.group() == 'a', 're.match alternation: leftmost match wins (like CPython)'\n\n# match with alternation + flags\nm = re.match(r'B|AB', 'ab', re.IGNORECASE)\nassert m is not None, 're.match alternation with IGNORECASE flag'\nassert m.group() == 'ab', 're.match alternation IGNORECASE: second alt matches at pos 0'\n\n# compiled match with alternation + flags\np = re.compile(r'B|AB', re.IGNORECASE)\nm = p.match('ab')\nassert m is not None, 'Pattern.match alternation with IGNORECASE flag'\nassert m.group() == 'ab', 'Pattern.match alternation IGNORECASE matches correctly'\n\n# === \\g<N> numeric backreference in replacement ===\nresult = re.sub(r'(\\w+)\\s+(\\w+)', r'\\g<2> \\g<1>', 'hello world')\nassert result == 'world hello', r'\\g<N> numeric backreference swaps groups'\n\nresult = re.sub(r'(\\w+)\\s+(\\w+)', r'\\g<0>', 'hello world')\nassert result == 'hello world', r'\\g<0> is the full match'\n\nresult = re.sub(r'(\\w+)', r'\\g<1>!', 'hello world')\nassert result == 'hello! world!', r'\\g<1> with suffix'\n\n# \\g<N> with multiple replacements in one string\nresult = re.sub(r'(\\w+)\\s+(\\w+)\\s+(\\w+)', r'\\g<3>-\\g<2>-\\g<1>', 'a b c')\nassert result == 'c-b-a', r'\\g<N> multiple groups reversed'\n\n# \\g<N> mixed with \\1 style backrefs\nresult = re.sub(r'(\\w+)\\s+(\\w+)', r'\\1-\\g<2>', 'hello world')\nassert result == 'hello-world', r'\\1 and \\g<2> mixed in replacement'\n\n# \\g<N> mixed with literal $\nresult = re.sub(r'(\\w+)', r'$\\g<1>$', 'hi')\nassert result == '$hi$', r'\\g<1> with literal $ signs'\n\n# === \\g<name> named backreference in replacement ===\nresult = re.sub(r'(?P<first>\\w+)\\s+(?P<second>\\w+)', r'\\g<second> \\g<first>', 'hello world')\nassert result == 'world hello', r'\\g<name> named backreference swaps groups'\n\n# \\g<name> on compiled pattern\np = re.compile(r'(?P<word>\\w+)')\nresult = p.sub(r'[\\g<word>]', 'hello world')\nassert result == '[hello] [world]', r'compiled pattern \\g<name> backreference'\n\n# \\g<name> mixed with \\g<N>\nresult = re.sub(r'(?P<a>\\w+)\\s+(\\w+)', r'\\g<a>-\\g<2>', 'hello world')\nassert result == 'hello-world', r'\\g<name> and \\g<N> mixed'\n\n# === \\g combined with other replacement features ===\nresult = re.sub(r'(\\w+)', r'[\\g<1>]', 'hi')\nassert result == '[hi]', r'\\g<1> with surrounding literal brackets'\n\n# compiled pattern with \\g\np = re.compile(r'(\\w+)\\s+(\\w+)')\nresult = p.sub(r'\\g<2>-\\g<1>', 'hello world')\nassert result == 'world-hello', r'compiled pattern \\g<N> backreference'\n\n# === Bool as int in re functions ===\n# bool as flags (True=1, False=0)\nm = re.search(r'hello', 'HELLO', False)\nassert m is None, 'search flags=False (0) is case-sensitive'\n\nm = re.match(r'hello', 'HELLO', False)\nassert m is None, 'match flags=False is case-sensitive'\n\nm = re.fullmatch(r'hello', 'HELLO', False)\nassert m is None, 'fullmatch flags=False is case-sensitive'\n\nresult = re.findall(r'hello', 'HELLO hello', False)\nassert result == ['hello'], 'findall flags=False is case-sensitive'\n\np = re.compile(r'hello', False)\nassert p.flags & re.IGNORECASE == 0, 'compile with flags=False has no IGNORECASE'\n\np = re.compile(r'hello', True)\nassert p.flags & 1 != 0, 'compile with flags=True stores 1'\n\n# bool as count in re.sub (True=1 replacement, False=0=all)\nresult = re.sub(r'\\d', 'X', '123', True)\nassert result == 'X23', 'count=True replaces only first match'\n\nresult = re.sub(r'\\d', 'X', '123', False)\nassert result == 'XXX', 'count=False (0) replaces all matches'\n\n# bool as count in Pattern.sub\np = re.compile(r'\\d')\nresult = p.sub('X', '123', True)\nassert result == 'X23', 'Pattern.sub count=True replaces only first'\n\nresult = p.sub('X', '123', False)\nassert result == 'XXX', 'Pattern.sub count=False replaces all'\n\n# === re.error alias (same as re.PatternError) ===\nassert re.error is re.PatternError, 're.error is alias for re.PatternError'\ntry:\n    re.compile('(unclosed')\n    assert False, 'should raise'\nexcept re.error as e:\n    assert len(str(e)) > 0, 're.error catches PatternError'\n\n# === re.escape() ===\nassert re.escape('hello') == 'hello', 're.escape leaves alphanumeric unchanged'\nassert re.escape('hello world!') == 'hello\\\\ world!', 're.escape escapes space but not !'\nassert re.escape('a.b+c*d?e') == 'a\\\\.b\\\\+c\\\\*d\\\\?e', 're.escape escapes regex metacharacters'\nassert re.escape('') == '', 're.escape on empty string'\nassert re.escape('[test]') == '\\\\[test\\\\]', 're.escape escapes brackets'\nassert re.escape('price: $10') == 'price:\\\\ \\\\$10', 're.escape escapes space and dollar'\nassert re.escape('a_b') == 'a_b', 're.escape preserves underscores'\n\n# re.escape result works as a literal pattern\ntext = 'price is $10.00 (USD)'\nescaped = re.escape('$10.00')\nm = re.search(escaped, text)\nassert m is not None, 'escaped pattern matches literally'\nassert m.group() == '$10.00', 'escaped pattern matches the exact string'\n\n# === re.sub() with keyword arguments ===\nresult = re.sub(r'\\d+', 'X', 'a1 b2 c3', count=1)\nassert result == 'aX b2 c3', 're.sub with count kwarg'\n\nresult = re.sub(r'hello', 'X', 'Hello HELLO hello', count=0, flags=re.IGNORECASE)\nassert result == 'X X X', 're.sub with flags kwarg'\n\n# Pattern.sub with count kwarg\np = re.compile(r'\\d+')\nresult = p.sub('X', 'a1 b2 c3', count=1)\nassert result == 'aX b2 c3', 'Pattern.sub with count kwarg'\n\n# === re.split() ===\nresult = re.split(r'\\s+', 'hello world foo')\nassert result == ['hello', 'world', 'foo'], 're.split basic'\n\nresult = re.split(r'[,;]', 'a,b;c')\nassert result == ['a', 'b', 'c'], 're.split on multiple delimiters'\n\nresult = re.split(r'\\s+', 'hello world foo', maxsplit=1)\nassert result == ['hello', 'world foo'], 're.split with maxsplit=1'\n\nresult = re.split(r'\\s+', 'hello')\nassert result == ['hello'], 're.split with no matches'\n\nresult = re.split(r'\\s+', '')\nassert result == [''], 're.split on empty string'\n\n# Pattern.split\np = re.compile(r'[,;]')\nresult = p.split('a,b;c')\nassert result == ['a', 'b', 'c'], 'Pattern.split basic'\n\nresult = p.split('a,b;c', maxsplit=1)\nassert result == ['a', 'b;c'], 'Pattern.split with maxsplit kwarg'\n\n# === re.finditer() ===\nmatches = list(re.finditer(r'\\d+', 'a1 b22 c333'))\nassert len(matches) == 3, 'finditer returns 3 matches'\nassert matches[0].group() == '1', 'finditer match 0'\nassert matches[1].group() == '22', 'finditer match 1'\nassert matches[2].group() == '333', 'finditer match 2'\n\n# finditer with no matches\nmatches = list(re.finditer(r'\\d+', 'no numbers'))\nassert len(matches) == 0, 'finditer with no matches returns empty'\n\n# finditer iteration\ngroups = [m.group() for m in re.finditer(r'\\w+', 'hello world')]\nassert groups == ['hello', 'world'], 'finditer in list comprehension'\n\n# Pattern.finditer\np = re.compile(r'\\d+')\nmatches = list(p.finditer('a1 b22'))\nassert len(matches) == 2, 'Pattern.finditer returns 2 matches'\nassert matches[0].group() == '1', 'Pattern.finditer match 0'\nassert matches[1].group() == '22', 'Pattern.finditer match 1'\n\n# finditer with capture groups\nmatches = list(re.finditer(r'(\\w+)=(\\w+)', 'a=1 b=2'))\nassert len(matches) == 2, 'finditer with groups returns 2 matches'\nassert matches[0].group(1) == 'a', 'finditer group 1 of match 0'\nassert matches[0].group(2) == '1', 'finditer group 2 of match 0'\nassert matches[1].group(1) == 'b', 'finditer group 1 of match 1'\n"
  },
  {
    "path": "crates/monty/test_cases/re__grouping.py",
    "content": "# Tests for the re (regular expression) module - capture groups and grouping\n\nimport re\n\n# === Capture groups ===\nm = re.search(r'(\\w+)@(\\w+)', 'user@host')\nassert m is not None, 're.search with groups finds a match'\nassert m.group(0) == 'user@host', 'group(0) is the full match'\nassert m.group(1) == 'user', 'group(1) is first capture'\nassert m.group(2) == 'host', 'group(2) is second capture'\nassert m.groups() == ('user', 'host'), 'groups() returns tuple of captures'\n\n# === group start/end/span with capture groups ===\nm = re.search(r'(\\w+)@(\\w+)', 'email: user@host here')\nassert m is not None, 'search with groups finds match'\nassert m.start(0) == 7, 'start(0) is full match start'\nassert m.end(0) == 16, 'end(0) is full match end'\nassert m.start(1) == 7, 'start(1) is group 1 start'\nassert m.end(1) == 11, 'end(1) is group 1 end'\nassert m.span(1) == (7, 11), 'span(1) is group 1 span'\nassert m.start(2) == 12, 'start(2) is group 2 start'\nassert m.end(2) == 16, 'end(2) is group 2 end'\nassert m.span(2) == (12, 16), 'span(2) is group 2 span'\n\n# === re.findall() with one group ===\nresult = re.findall(r'(\\d+)', 'a1 b22 c333')\nassert result == ['1', '22', '333'], 'findall with one group returns list of group strings'\n\n# === re.findall() with multiple groups ===\nresult = re.findall(r'(\\w+)=(\\w+)', 'a=1 b=2')\nassert result == [('a', '1'), ('b', '2')], 'findall with multiple groups returns list of tuples'\n\n# === No groups: groups() returns empty tuple ===\nm = re.search(r'\\d+', '42')\nassert m is not None, 'search with no groups finds match'\nassert m.groups() == (), 'groups() with no capture groups returns empty tuple'\n\n# === Backreferences ===\nm = re.search(r'(\\w+)\\s+\\1', 'hello hello')\nassert m is not None, 'backreference finds repeated word'\nassert m.group(0) == 'hello hello', 'backreference full match'\nassert m.group(1) == 'hello', 'backreference group'\n\n# === Invalid group index ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.group(2)\n    assert False, 'Accessing invalid group index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group'\ntry:\n    m.group('foo')\n    assert False, 'Accessing group with non-integer index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group'\n\n# === re.sub() replacement with backreferences ===\nresult = re.sub(r'(\\w+)=(\\w+)', r'\\2=\\1', 'a=1 b=2')\nassert result == '1=a 2=b', 're.sub with backreferences swaps groups'\n\n# === Negative group index ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.group(-1)\n    assert False, 'Negative group index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'negative group raises IndexError with \"no such group\" message'\n\n# === Out-of-range group index ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.group(999)\n    assert False, 'Out-of-range group index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'out-of-range group raises IndexError with \"no such group\" message'\n\n# === Non-integer group argument: float ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.group(1.5)\n    assert False, 'Float group argument should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'float group arg raises IndexError'\n\n# === Non-integer group argument: string ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.group('1')\n    assert False, 'String group argument should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'string group arg raises IndexError'\n\n# === Non-integer group argument: None ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.group(None)\n    assert False, 'None group argument should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'None group arg raises IndexError'\n\n# === Negative group index for start() ===\nm = re.search(r'(\\w+)@(\\w+)', 'user@host')\nassert m is not None, 'search with groups finds match'\ntry:\n    m.start(-1)\n    assert False, 'Negative start index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'negative start raises IndexError with \"no such group\" message'\n\n# === Out-of-range group index for start() ===\nm = re.search(r'(\\w+)@(\\w+)', 'user@host')\nassert m is not None, 'search with groups finds match'\ntry:\n    m.start(999)\n    assert False, 'Out-of-range start index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'out-of-range start raises IndexError'\n\n# === Non-integer argument for start() ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.start(1.5)\n    assert False, 'Float start argument should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'float start arg raises IndexError'\n\n# === Negative group index for end() ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.end(-2)\n    assert False, 'Negative end index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'negative end raises IndexError with \"no such group\" message'\n\n# === Out-of-range group index for end() ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.end(100)\n    assert False, 'Out-of-range end index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'out-of-range end raises IndexError'\n\n# === Non-integer argument for end() ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.end('0')\n    assert False, 'String end argument should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'string end arg raises IndexError'\n\n# === Negative group index for span() ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.span(-1)\n    assert False, 'Negative span index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'negative span raises IndexError with \"no such group\" message'\n\n# === Out-of-range group index for span() ===\nm = re.search(r'(\\w+)@(\\w+)', 'user@host')\nassert m is not None, 'search with groups finds match'\ntry:\n    m.span(5)\n    assert False, 'Out-of-range span index should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'out-of-range span raises IndexError'\n\n# === Non-integer argument for span() ===\nm = re.search(r'(\\w+)', 'hello')\nassert m is not None, 'search with group finds match'\ntry:\n    m.span(None)\n    assert False, 'None span argument should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'None span arg raises IndexError'\n\n# === Accessing unmatched optional group returns None ===\n# Optional groups that don't match return None instead of raising an error\nm = re.search(r'(\\w+)?@(\\w+)', '@host')\nassert m is not None, 'search with optional group finds match'\nassert m.group(1) is None, 'unmatched optional group returns None'\nassert m.start(1) == -1, 'start of unmatched optional group returns -1'\nassert m.end(1) == -1, 'end of unmatched optional group returns -1'\nassert m.span(1) == (-1, -1), 'span of unmatched optional group returns (-1, -1)'\n\n# === Named group access with m.group('name') ===\nm = re.search(r'(?P<first>\\w+)\\s+(?P<second>\\w+)', 'hello world')\nassert m is not None, 'named group search finds match'\nassert m.group('first') == 'hello', \"group('first') returns first named group\"\nassert m.group('second') == 'world', \"group('second') returns second named group\"\nassert m.group(1) == 'hello', 'named group is also accessible by index'\nassert m.group(2) == 'world', 'named group is also accessible by index'\nassert m.group(0) == 'hello world', 'group(0) still returns full match'\n\n# Named group with invalid name\ntry:\n    m.group('nonexistent')\n    assert False, 'non-existent named group should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'non-existent named group error message'\n\n# === m.group() with multiple arguments ===\nm = re.search(r'(\\w+)\\s+(\\w+)\\s+(\\w+)', 'a b c')\nassert m is not None, 'multi-group search finds match'\nresult = m.group(1, 2)\nassert result == ('a', 'b'), 'group(1, 2) returns tuple of two groups'\n\nresult = m.group(1, 2, 3)\nassert result == ('a', 'b', 'c'), 'group(1, 2, 3) returns tuple of three groups'\n\nresult = m.group(0, 1)\nassert result == ('a b c', 'a'), 'group(0, 1) includes full match'\n\n# === m.groupdict() ===\nm = re.search(r'(?P<first>\\w+)\\s+(?P<second>\\w+)', 'hello world')\nassert m is not None, 'named group search for groupdict'\nd = m.groupdict()\nassert d == {'first': 'hello', 'second': 'world'}, 'groupdict returns correct dict'\n\n# groupdict with no named groups\nm = re.search(r'(\\w+)\\s+(\\w+)', 'hello world')\nassert m is not None, 'unnamed group search for groupdict'\nd = m.groupdict()\nassert d == {}, 'groupdict with no named groups returns empty dict'\n\n# groupdict with unmatched optional named group\nm = re.search(r'(?P<first>\\w+)?@(?P<second>\\w+)', '@host')\nassert m is not None, 'optional named group search for groupdict'\nd = m.groupdict()\nassert d == {'first': None, 'second': 'host'}, 'groupdict includes unmatched named groups as None'\n"
  },
  {
    "path": "crates/monty/test_cases/re__match.py",
    "content": "# Tests for the re (regular expression) module - Match object\n\nimport re\n\n# === Match .string attribute ===\nm = re.search('hello', 'say hello')\nassert m is not None, 'search finds match for .string test'\nassert m.string == 'say hello', '.string returns the input string'\n\n# === Match truthiness ===\nm = re.search(r'\\d+', '123')\nassert m, 'Match objects are truthy'\n\n# === Match repr ===\nm = re.search(r'\\d+', 'abc 42 def')\nassert repr(m) == \"<re.Match object; span=(4, 6), match='42'>\", 'Match repr'\n\n# === Object basic ===\nassert bool(re.search(r'\\w+', 'hello'))\nassert isinstance(re.search(r'\\w+', 'hello'), re.Match), 're.search returns re.Match instance'\nassert str(type(re.search(r'\\w+', 'hello'))) == \"<class 're.Match'>\", 'type of search match is re.Match'\n\n# === Match equality - Match objects are not comparable ===\nm1 = re.search(r'\\w+', 'hello')\nm2 = re.search(r'\\w+', 'hello')\nassert (m1 == m2) == False, 'different Match objects are not equal'\nassert m1 != m2, 'Match objects with same content are not equal'\n\n# === Match methods are reusable on same object ===\nm = re.search(r'(\\w+)@(\\w+)', 'user@host')\nassert m is not None, 'search finds match'\nassert m.group(0) == 'user@host', 'first call to group(0) works'\nassert m.group(0) == 'user@host', 'second call to group(0) works'\nassert m.group(1) == 'user', 'call to group(1) works'\nassert m.start(1) == 0, 'start(1) works'\nassert m.end(1) == 4, 'end(1) works'\nassert m.span(0) == (0, 9), 'span(0) works'\n\n# === .string attribute is accessible multiple times ===\nm = re.search(r'hello', 'say hello world')\nassert m is not None, 'search finds match'\nassert m.string == 'say hello world', 'first access to .string works'\nassert m.string == 'say hello world', 'second access to .string works'\n\n# === Match object with empty string ===\nm = re.search(r'', 'hello')\nassert m is not None, 'empty pattern matches'\nassert m.string == 'hello', '.string returns input for empty match'\nassert m.group(0) == '', 'empty match group(0) is empty string'\n\n# === Match object from match() function ===\nm = re.match(r'(\\w+)', 'hello world')\nassert m is not None, 're.match finds match'\nassert m.group(0) == 'hello', 'match() returns correct match'\nassert m.start(0) == 0, 'match starts at position 0'\nassert m.string == 'hello world', '.string returns full input'\n\n# === Match object from fullmatch() function ===\nm = re.fullmatch(r'\\w+', 'hello')\nassert m is not None, 're.fullmatch finds exact match'\nassert m.group(0) == 'hello', 'fullmatch returns correct match'\nassert m.start(0) == 0, 'fullmatch starts at position 0'\nassert m.end(0) == 5, 'fullmatch ends at correct position'\n\n# === Match repr basic format ===\nm = re.search(r'\\d+', 'abc 42 def')\nassert repr(m) == \"<re.Match object; span=(4, 6), match='42'>\", 'Match repr basic format'\n\nm = re.search(r'\\w+', 'hello')\nassert repr(m) == \"<re.Match object; span=(0, 5), match='hello'>\", 'Match repr at start'\n\nm = re.search(r'', 'hello')\nassert repr(m) == \"<re.Match object; span=(0, 0), match=''>\", 'Match repr empty match'\n\n# === Match repr with special characters ===\np = re.compile(r'a.b', re.DOTALL)\nm = p.search('a\\nb')\nassert m is not None, 'DOTALL match for repr test'\nr = repr(m)\nassert r == \"<re.Match object; span=(0, 3), match='a\\\\nb'>\", 'Match repr escapes newline'\n\nm = re.search(r'a.b', 'a\\tb')\nassert m is not None, 'tab match for repr test'\nr = repr(m)\nassert r == \"<re.Match object; span=(0, 3), match='a\\\\tb'>\", 'Match repr escapes tab'\n\n# backslash in matched text\nm = re.search(r'a.b', 'a\\\\b')\nassert m is not None, 'backslash match for repr test'\nr = repr(m)\nassert r == \"<re.Match object; span=(0, 3), match='a\\\\\\\\b'>\", 'Match repr escapes backslash'\n\n# carriage return in matched text\np = re.compile(r'a.b', re.DOTALL)\nm = p.search('a\\rb')\nassert m is not None, 'carriage return match for repr test'\nr = repr(m)\nassert r == \"<re.Match object; span=(0, 3), match='a\\\\rb'>\", 'Match repr escapes carriage return'\n\n# single quote in matched text — repr switches to double quotes\nm = re.search(r'a.b', \"a'b\")\nassert m is not None, 'single quote match for repr test'\nr = repr(m)\nassert r == '<re.Match object; span=(0, 3), match=\"a\\'b\">', 'Match repr handles single quote'\n\n# double quote in matched text — repr uses single quotes\nm = re.search(r'a.b', 'a\"b')\nassert m is not None, 'double quote match for repr test'\nr = repr(m)\nassert r == \"<re.Match object; span=(0, 3), match='a\\\"b'>\", 'Match repr handles double quote'\n\n# === Pattern repr ===\np = re.compile('hello')\nassert repr(p) == \"re.compile('hello')\", 'Pattern repr simple string'\n\np = re.compile(r'\\n\\t')\nassert repr(p) == \"re.compile('\\\\\\\\n\\\\\\\\t')\", 'Pattern repr with escape sequences in pattern'\n\n# === Bool as group index ===\nm = re.search(r'(\\w+)\\s+(\\w+)', 'hello world')\nassert m is not None, 'search for bool group test'\nassert m.group(True) == 'hello', 'group(True) is group(1)'\nassert m.group(False) == 'hello world', 'group(False) is group(0)'\nassert m.start(True) == 0, 'start(True) is start(1)'\nassert m.end(True) == 5, 'end(True) is end(1)'\nassert m.span(True) == (0, 5), 'span(True) is span(1)'\nassert m.span(False) == (0, 11), 'span(False) is span(0)'\n\n# === m[N] subscript access ===\nm = re.search(r'(\\w+)\\s+(\\w+)', 'hello world')\nassert m is not None, 'search for subscript test'\nassert m[0] == 'hello world', 'm[0] is full match'\nassert m[1] == 'hello', 'm[1] is first group'\nassert m[2] == 'world', 'm[2] is second group'\n\n# subscript with named groups\nm = re.search(r'(?P<first>\\w+)\\s+(?P<second>\\w+)', 'hello world')\nassert m is not None, 'search for named subscript test'\nassert m['first'] == 'hello', \"m['first'] accesses named group\"\nassert m['second'] == 'world', \"m['second'] accesses named group\"\nassert m[1] == 'hello', 'm[1] also works with named groups'\n\n# subscript with invalid index\ntry:\n    m[99]\n    assert False, 'out-of-range subscript should raise IndexError'\nexcept IndexError as e:\n    assert str(e) == 'no such group', 'subscript IndexError message'\n"
  },
  {
    "path": "crates/monty/test_cases/recursion__deep_drop.py",
    "content": "# Test that dropping deeply nested containers doesn't crash (stack overflow).\n# Heap::dec_ref recurses in Rust when freeing child references, so deeply\n# nested containers can overflow the Rust call stack during cleanup.\n# CPython handles this fine (its dealloc uses an iterative trashcan mechanism).\n# Once fixed (iterative dec_ref), this should work without crashing.\n\n# === Deep list drop ===\nx = [1]\nfor _ in range(10000):\n    x = [x]\nx = None  # triggers recursive dec_ref chain\nassert True, 'survived deep list drop'\n\n# === Deep tuple drop ===\ny = (1,)\nfor _ in range(10000):\n    y = (y,)\ny = None  # triggers recursive dec_ref chain\nassert True, 'survived deep tuple drop'\n\n# === Deep dict drop ===\nz = {'a': 1}\nfor _ in range(10000):\n    z = {'a': z}\nz = None  # triggers recursive dec_ref chain\nassert True, 'survived deep dict drop'\n"
  },
  {
    "path": "crates/monty/test_cases/recursion__deep_eq.py",
    "content": "# Test that deeply nested lists don't crash during equality comparison\n# Monty raises RecursionError at depth limit, CPython handles in C code\na = []\nb = []\nfor _ in range(30):  # Use lower depth that works within unified recursion limit\n    a = [a]\n    b = [b]\n\n# Should not crash\nresult = a == b\nassert isinstance(result, bool), 'comparison should return a bool'\nassert result == True, 'structurally equal nested lists should be equal'\n\n# Test non-equal nested lists\nc = []\nfor _ in range(30):\n    c = [c]\nc = [1]  # Make the innermost different\nfor _ in range(29):\n    c = [c]\n\nresult2 = a == c\nassert result2 == False, 'structurally different nested lists should not be equal'\n"
  },
  {
    "path": "crates/monty/test_cases/recursion__deep_hash.py",
    "content": "# Test that hashing deeply nested containers raises RecursionError instead\n# of crashing with a Rust stack overflow.\n\n# === Deep tuple hash ===\nx = (1,)\nfor _ in range(10000):\n    x = (x,)\n\ntry:\n    h = hash(x)\n    assert isinstance(h, int), 'hash should return an int'\nexcept RecursionError:\n    pass  # acceptable if depth guard triggers\n\n# === Deep frozenset hash ===\ny = frozenset({1})\nfor _ in range(10000):\n    y = frozenset({y})\n\ntry:\n    h = hash(y)\n    assert isinstance(h, int), 'hash should return an int'\nexcept RecursionError:\n    pass  # acceptable if depth guard triggers\n\n# === Deep tuple as dict key (triggers hash) ===\nz = (1,)\nfor _ in range(10000):\n    z = (z,)\n\nd = {}\ntry:\n    d[z] = 'value'\nexcept RecursionError:\n    pass  # acceptable if depth guard triggers\n\n# === Deep tuple as set element (triggers hash) ===\nw = (1,)\nfor _ in range(10000):\n    w = (w,)\n\ns = set()\ntry:\n    s.add(w)\nexcept RecursionError:\n    pass  # acceptable if depth guard triggers\n"
  },
  {
    "path": "crates/monty/test_cases/recursion__deep_repr.py",
    "content": "# Test that deeply nested lists don't crash during repr()\n# Monty truncates with \"...\" at depth limit, CPython handles in C code\nx = []\nfor _ in range(200):\n    x = [x]\n\n# Should not crash - either returns full repr or truncated with \"...\"\nresult = repr(x)\nassert isinstance(result, str), 'repr should return a string'\nassert result.startswith('['), 'repr should start with ['\n# Either full repr or truncated\nassert result.endswith(']') or '...' in result, 'repr should end with ] or contain ...'\n"
  },
  {
    "path": "crates/monty/test_cases/recursion__function_depth.py",
    "content": "# Test that recursive function calls hit the recursion limit\n# This uses Python function call recursion which both CPython and Monty limit\n\n\ndef recurse(n):\n    if n == 0:\n        return 0\n    return recurse(n - 1) + 1\n\n\n# This should raise RecursionError in both interpreters\nrecurse(2000)\n# Raise=RecursionError('maximum recursion depth exceeded')\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__cycle_mutual_reference.py",
    "content": "# Mutual reference cycle: a contains b, b contains a\n# This creates a cycle where:\n#   - a has refcount 2 (variable 'a' + being inside b)\n#   - b has refcount 2 (variable 'b' + being inside a)\n# Without cycle detection, when both variables go out of scope:\n#   - a's refcount drops to 1 (still in b)\n#   - b's refcount drops to 1 (still in a)\n#   - Neither reaches 0, neither is freed (memory leak)\n#\n# NOTE: We return len(b) instead of b because repr(b) would cause infinite\n# recursion / stack overflow (a separate bug - Python handles this by printing [...]\n# for cyclic references)\na = []\nb = []\na.append(b)\nb.append(a)\nlen(b)\n# ref-counts={'a': 2, 'b': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__cycle_self_reference.py",
    "content": "# Self-referential list: a contains itself\n# This creates a cycle where a's refcount is 2 (variable + self-reference)\n# Without cycle detection, when 'a' goes out of scope, refcount drops to 1\n# but the object is never freed (memory leak)\n#\n# NOTE: We return len(a) instead of a because repr(a) would cause infinite\n# recursion / stack overflow (a separate bug - Python handles this by printing [...]\n# for cyclic references)\na = []\na.append(a)\nlen(a)\n# ref-counts={'a': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__dict_basic.py",
    "content": "k = 'key'\nv = [1, 2]\nd = {k: v}\nd\n# ref-counts={'v': 2, 'd': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__dict_get.py",
    "content": "v = [1, 2]\nd = {0: v}\nx = d[0]\nx\n# ref-counts={'v': 4, 'd': 1, 'x': 4}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__dict_keys_and.py",
    "content": "inner = ('x',)\nwrapped = (inner,)\nempty = []\nrhs = [wrapped, empty]\n\ntry:\n    {}.keys() & rhs\n    assert False, 'dict_keys intersection should reject unhashable iterable items'\nexcept TypeError as e:\n    assert str(e) == \"cannot use 'list' as a set element (unhashable type: 'list')\", (\n        'dict_keys intersection should surface the recoverable set-element hash error'\n    )\n\n# ref-counts={'inner': 2, 'wrapped': 2, 'empty': 2, 'rhs': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__dict_overwrite.py",
    "content": "v1 = [1]\nv2 = [2]\nd = {0: v1}\nd[0] = v2\nd\n# ref-counts={'v1': 1, 'v2': 2, 'd': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__gather_cleanup.py",
    "content": "# Test that GatherFuture and coroutines are properly cleaned up after gather completes.\n# The strict matching check will fail if the GatherFuture leaks (heap_count > unique_refs).\nimport asyncio\n\n\nasync def task1():\n    return 1\n\n\nasync def task2():\n    return 2\n\n\nresult = await asyncio.gather(task1(), task2())  # pyright: ignore\nresult\n# ref-counts={'result': 2, 'asyncio': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__gather_exception.py",
    "content": "# Test that GatherFuture and coroutines are properly cleaned up when a task raises.\n# When one task fails, sibling tasks should be cancelled and all resources freed.\nimport asyncio\n\n\nasync def task_ok():\n    return 1\n\n\nasync def task_fail():\n    raise ValueError('task failed')\n\n\ntry:\n    result = await asyncio.gather(task_ok(), task_fail())  # pyright: ignore\nexcept ValueError:\n    pass\n# ref-counts={'asyncio': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__gather_nested_cancel.py",
    "content": "# Test that nested GatherFuture is properly cleaned up when outer task is cancelled.\n# When one task in an outer gather fails, sibling tasks (including those with inner gathers)\n# should be cancelled and all GatherFutures properly cleaned up.\nimport asyncio\n\n\nasync def inner_task():\n    return 1\n\n\nasync def task_with_inner_gather():\n    # This inner gather should be cancelled when the outer gather fails\n    result = await asyncio.gather(inner_task(), inner_task())\n    return result\n\n\nasync def task_fail():\n    raise ValueError('outer task failed')\n\n\ntry:\n    result = await asyncio.gather(task_with_inner_gather(), task_fail())  # pyright: ignore\nexcept ValueError:\n    pass\n# ref-counts={'asyncio': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__immediate_skipped.py",
    "content": "x = 42\ny = [1, 2]\ny\n# ref-counts={'y': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__keyword_only_kwarg_arity_errors.py",
    "content": "# Tests cleanup when keyword-only parsers reject calls before consuming kwargs.\n#\n# Shared kwarg parsing helpers are safe only if callers guard owned kwargs before\n# any early arity/type errors. These handled exceptions should not leak the\n# heap-backed kwarg values.\n\nsorted_key = ['sorted-key']\nsort_key = ['list-sort-key']\nitems = [3, 2, 1]\n\ntry:\n    sorted(key=sorted_key)\n    assert False, 'sorted() with no positional args should raise TypeError'\nexcept TypeError as e:\n    assert e.args == ('sorted expected 1 argument, got 0',), 'sorted() arity error should match CPython'\n\ntry:\n    items.sort(1, key=sort_key)\n    assert False, 'list.sort() should reject positional args before consuming kwargs'\nexcept TypeError:\n    pass\n\n# The handled exceptions above must not retain references to the kwarg payloads.\n# ref-counts={'sorted_key': 1, 'sort_key': 1, 'items': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__kwargs_unpacking.py",
    "content": "# Tests reference counting correctness for **kwargs unpacking\n\n\ndef receive_kwargs(a, b, c):\n    return a\n\n\n# === Heap-allocated values in unpacked dict ===\n# All heap objects must be directly referenced by variables for strict matching\nlist_a = [1, 2, 3]\nlist_c = [4, 5]\nkwargs_dict = {'a': list_a, 'b': 'hello', 'c': list_c}\nresult = receive_kwargs(**kwargs_dict)\nassert result == [1, 2, 3], 'received list via **kwargs'\nassert result is list_a, 'should be same object'\n\n# Second call to verify dict reuse works\nresult2 = receive_kwargs(**kwargs_dict)\nassert result2 is list_a, 'second call returns same object'\n\n# list_a: 5 refs (list_a var, kwargs_dict['a'], result, result2, final expr)\n# list_c: 2 refs (list_c var, kwargs_dict['c'])\n# kwargs_dict: 1 ref\n# result: 5 refs (same object as list_a)\n# result2: 5 refs (same object as list_a)\nresult2\n# ref-counts={'list_a': 5, 'list_c': 2, 'kwargs_dict': 1, 'result': 5, 'result2': 5}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__list_append_multiple.py",
    "content": "item = [1]\nlst = []\nlst.append(item)\nlst.append(item)\nlst\n# ref-counts={'item': 3, 'lst': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__list_append_ref.py",
    "content": "item = [1]\nlst = []\nlst.append(item)\nlst\n# ref-counts={'item': 2, 'lst': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__list_concat.py",
    "content": "a = [1]\nb = [2]\nc = a + b\nc\n# ref-counts={'a': 1, 'b': 1, 'c': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__list_getitem.py",
    "content": "item = [1, 2]\nlst = [item]\nx = lst[0]\nx\n# ref-counts={'item': 4, 'lst': 1, 'x': 4}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__list_iadd.py",
    "content": "a = [1]\nb = [2]\na += b\na\n# ref-counts={'a': 2, 'b': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__min_max_key_error_paths.py",
    "content": "# Tests reference counting when min()/max() key functions raise on the first item.\n#\n# Both the iterable form and the multiple-argument form must guard the initial\n# candidate before calling the user-provided key function. Otherwise the first\n# winner leaks when key evaluation raises before the comparison loop starts.\n\n\ndef raising_key(value):\n    raise ValueError('boom')\n\n\nitem_iter = ['iter']\nitem_multi = ['multi']\nother_multi = ['other']\n\ntry:\n    max([item_iter], key=raising_key)\n    assert False, 'max(iterable, key=raising_key) should raise ValueError'\nexcept ValueError as e:\n    assert e.args == ('boom',), 'max iterable key error should propagate unchanged'\n\ntry:\n    min(item_multi, other_multi, key=raising_key)\n    assert False, 'min(arg1, arg2, key=raising_key) should raise ValueError'\nexcept ValueError as e:\n    assert e.args == ('boom',), 'min multi-arg key error should propagate unchanged'\n\n# The temporary argument container for max() and the current winner slots in\n# both builtin code paths must be released after the handled exception.\n# ref-counts={'item_iter': 1, 'item_multi': 1, 'other_multi': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__nested_list.py",
    "content": "inner = [1, 2]\nouter = [inner, inner]\nouter\n# ref-counts={'inner': 3, 'outer': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__re_pattern_sub_error_paths.py",
    "content": "# Tests reference counting on Pattern.sub error paths.\n#\n# The positional arg iterator and extra args must be properly dropped even\n# when Pattern.sub raises due to too many args or a bad count type.\n# These paths previously leaked because pos.next().is_some() consumed a\n# Value without dropping it.\n\nimport re\n\n# Use lists as heap-allocated values we can track\nrepl_list = ['replacement']\ninput_list = ['the input']\np = re.compile('hello')\n\n# Exercise error path: too many positional arguments\ntry:\n    p.sub('repl', 'string', 0, 'extra')\nexcept TypeError:\n    pass\n\n# Exercise error path: bad count type\ntry:\n    p.sub('repl', 'string', 'bad')\nexcept TypeError:\n    pass\n\n# Exercise negative count path (early return)\nresult = p.sub('repl', 'hello', -1)\nassert result == 'hello', 'negative count returns input unchanged'\n\n# repl_list: 1 (variable)\n# input_list: 1 (variable)\n# p: 1 (variable)\n# re: 1 (module)\n# result: 2 (variable + final expression)\nresult\n# ref-counts={'repl_list': 1, 'input_list': 1, 'p': 1, 're': 1, 'result': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__re_search_match.py",
    "content": "# Tests reference counting for re.search, re.match, and re.fullmatch.\n#\n# Verifies that Match objects, Pattern objects, and intermediate strings\n# are correctly reference-counted through normal usage paths.\n# All heap objects must be directly referenced by variables for strict matching.\n\nimport re\n\n# Compile a pattern and run search — both pattern and match stay alive\np = re.compile(r'(\\w+)')\nm = p.search('hello world')\nassert m is not None, 'search finds match'\ngroup_str = m.group(0)\nassert group_str == 'hello', 'group(0) returns matched text'\n\n# Run fullmatch — exercises the compiled_fullmatch regex path\nm2 = p.fullmatch('hello')\nassert m2 is not None, 'fullmatch finds match'\nfull_str = m2.group(0)\nassert full_str == 'hello', 'fullmatch group(0) returns matched text'\n\n# findall returns a list — keep individual elements in variables\n# so strict matching passes (all heap objects must be reachable)\nresults = p.findall('a b c')\nassert results == ['a', 'b', 'c'], 'findall returns list of matches'\nr0 = results[0]\nr1 = results[1]\nr2 = results[2]\n\n# p: 1, m: 1, group_str: 1, m2: 1, full_str: 1\n# results: 1, r0: 2 (var + list), r1: 2 (var + list), r2: 2 (var + list + final expr)\n# re: 1\nr2\n# ref-counts={'p': 1, 'm': 1, 'group_str': 1, 'm2': 1, 'full_str': 1, 'results': 1, 'r0': 2, 'r1': 2, 'r2': 3, 're': 1}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__re_sub_error_paths.py",
    "content": "# Tests reference counting on re.sub error paths.\n#\n# The positional arg iterator and extra args must be properly dropped even\n# when re.sub raises due to too many args or a bad count type.\n# These paths previously leaked because pos.next().is_some() consumed a\n# Value without dropping it, and the pos iterator itself was unguarded.\n\nimport re\n\n# Use lists as heap-allocated values that we can track through error paths.\n# String literals may be interned and won't show up in heap ref counts.\nrepl_list = ['replacement']\ninput_list = ['the input']\n\n# Exercise error path: bad count type with heap-allocated args in scope\ntry:\n    re.sub('pattern', 'repl', 'input', 'bad')\nexcept TypeError:\n    pass\n\n# Exercise negative count path (early return, no regex compilation)\nresult = re.sub('pattern', 'repl', 'hello', -1)\nassert result == 'hello', 'negative count returns input unchanged'\n\n# All lists should still be alive and reachable\n# repl_list: 1 (variable)\n# input_list: 1 (variable)\n# re: 1 (module)\n# result: 2 (variable + final expression)\nresult\n# ref-counts={'repl_list': 1, 'input_list': 1, 're': 1, 'result': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__shared_reference.py",
    "content": "x = [1, 2, 3]\ny = x\nx\n# ref-counts={'x': 3, 'y': 3}\n"
  },
  {
    "path": "crates/monty/test_cases/refcount__single_list.py",
    "content": "x = [1, 2, 3]\nx\n# ref-counts={'x': 2}\n"
  },
  {
    "path": "crates/monty/test_cases/repr__cycle_detection.py",
    "content": "# Test cycle detection in repr for self-referential structures\n\n# Section 1: List self-reference\na = []\na.append(a)\nassert repr(a) == '[[...]]', 'list self-reference'\n\n# Section 2: Dict self-reference\nd = {}\nd['self'] = d\nassert repr(d) == \"{'self': {...}}\", 'dict self-reference'\n\n# Section 3: Composite - list containing dict containing original list\nc = []\ne = {'list': c}\nc.append(e)\nassert repr(c) == \"[{'list': [...]}]\", 'list containing dict cycle'\nassert repr(e) == \"{'list': [{...}]}\", 'dict containing list cycle'\n\n# Section 4: Multiple references to same cyclic object\nf = []\nf.append(f)\ng = [f, f]\nassert repr(g) == '[[[...]], [[...]]]', 'multiple refs to cyclic list'\n"
  },
  {
    "path": "crates/monty/test_cases/set__ops.py",
    "content": "# === Construction ===\ns = set()\nassert len(s) == 0, 'empty set len'\nassert s == set(), 'empty set equality'\n\ns = set([1, 2, 3])\nassert len(s) == 3, 'set from list len'\n\n# === Basic Methods ===\ns = set()\ns.add(1)\ns.add(2)\ns.add(1)  # duplicate\nassert len(s) == 2, 'add with duplicate'\n\n# === Discard and Remove ===\ns = set([1, 2, 3])\ns.discard(2)\nassert len(s) == 2, 'discard existing'\ns.discard(99)  # should not raise\nassert len(s) == 2, 'discard non-existing'\n\n# === Pop ===\ns = set([1])\nv = s.pop()\nassert v == 1, 'pop returns element'\nassert len(s) == 0, 'pop removes element'\n\n# === Clear ===\ns = set([1, 2, 3])\ns.clear()\nassert len(s) == 0, 'clear'\n\n# === Copy ===\ns = set([1, 2, 3])\ns2 = s.copy()\nassert s == s2, 'copy equality'\ns.add(4)\nassert s != s2, 'copy is independent'\n\n# === Update ===\ns = set([1, 2])\ns.update([2, 3, 4])\nassert len(s) == 4, 'update with list'\n\n# === Union ===\ns1 = set([1, 2])\ns2 = set([2, 3])\nu = s1.union(s2)\nassert len(u) == 3, 'union len'\n\n# === Intersection ===\ns1 = set([1, 2, 3])\ns2 = set([2, 3, 4])\ni = s1.intersection(s2)\nassert len(i) == 2, 'intersection len'\n\n# === Difference ===\ns1 = set([1, 2, 3])\ns2 = set([2, 3, 4])\nd = s1.difference(s2)\nassert len(d) == 1, 'difference len'\n\n# === Symmetric Difference ===\ns1 = set([1, 2, 3])\ns2 = set([2, 3, 4])\nsd = s1.symmetric_difference(s2)\nassert len(sd) == 2, 'symmetric_difference len'\n\n# === Binary operators ===\ns = {1, 2}\nt = {2, 3}\nfs = frozenset([2, 3])\n\nassert s & t == {2}, 'set & set works'\nassert s | t == {1, 2, 3}, 'set | set works'\nassert s ^ t == {1, 3}, 'set ^ set works'\nassert s - t == {1}, 'set - set works'\n\nassert s & fs == {2}, 'set & frozenset works'\nassert s | fs == {1, 2, 3}, 'set | frozenset works'\nassert s ^ fs == {1, 3}, 'set ^ frozenset works'\nassert s - fs == {1}, 'set - frozenset works'\n\nkeys = {'a': 1, 'b': 2}.keys()\nitems = {'a': 1, 'b': 2}.items()\nassert {'a'} & keys == {'a'}, 'set & dict_keys works'\nassert {'a'} | keys == {'a', 'b'}, 'set | dict_keys works'\nassert {('a', 1)} ^ items == {('b', 2)}, 'set ^ dict_items works'\nassert {('a', 1), ('b', 2)} - items == set(), 'set - dict_items works'\n\nassert type(s & fs).__name__ == 'set', 'set operators keep the left operand type'\n\ntry:\n    s & [1, 2]\n    assert False, 'set operators reject non-set rhs'\nexcept TypeError as e:\n    assert str(e) == \"unsupported operand type(s) for &: 'set' and 'list'\", 'set & rhs error matches CPython'\n\ntry:\n    s | [1, 2]\n    assert False, 'set union operator rejects non-set rhs'\nexcept TypeError as e:\n    assert str(e) == \"unsupported operand type(s) for |: 'set' and 'list'\", 'set | rhs error matches CPython'\n\ntry:\n    s ^ [1, 2]\n    assert False, 'set xor operator rejects non-set rhs'\nexcept TypeError as e:\n    assert str(e) == \"unsupported operand type(s) for ^: 'set' and 'list'\", 'set ^ rhs error matches CPython'\n\ntry:\n    s - [1, 2]\n    assert False, 'set subtraction operator rejects non-set rhs'\nexcept TypeError as e:\n    assert str(e) == \"unsupported operand type(s) for -: 'set' and 'list'\", 'set - rhs error matches CPython'\n\n# === Issubset ===\ns1 = set([1, 2])\ns2 = set([1, 2, 3])\nassert s1.issubset(s2) == True, 'issubset true'\nassert s2.issubset(s1) == False, 'issubset false'\n\n# === Issuperset ===\ns1 = set([1, 2, 3])\ns2 = set([1, 2])\nassert s1.issuperset(s2) == True, 'issuperset true'\nassert s2.issuperset(s1) == False, 'issuperset false'\n\n# === Isdisjoint ===\ns1 = set([1, 2])\ns2 = set([3, 4])\ns3 = set([2, 3])\nassert s1.isdisjoint(s2) == True, 'isdisjoint true'\nassert s1.isdisjoint(s3) == False, 'isdisjoint false'\n\n# === Bool ===\nassert bool(set()) == False, 'empty set is falsy'\nassert bool(set([1])) == True, 'non-empty set is truthy'\n\n# === repr ===\nassert repr(set()) == 'set()', 'empty set repr'\n\n# === Set literals ===\ns = {1, 2, 3}\nassert len(s) == 3, 'set literal len'\n\ns = {1, 1, 2, 2, 3}\nassert len(s) == 3, 'set literal deduplication'\n\n# Set literal with expressions\nx = 5\ns = {x, x + 1, x + 2}\nassert len(s) == 3, 'set literal with expressions'\n\n# === Set unpacking (PEP 448) ===\na = [1, 2]\nb = [3, 4]\nassert {*a} == {1, 2}, 'single set unpack from list'\nassert {*a, *b} == {1, 2, 3, 4}, 'double set unpack'\nassert {0, *a, 5} == {0, 1, 2, 5}, 'mixed set unpack'\nassert {*[]} == set(), 'unpack empty into set'\nassert {*(1, 2)} == {1, 2}, 'unpack tuple into set'\nassert {*{'a': 1, 'b': 2}} == {'a', 'b'}, 'unpack dict keys into set'\nassert {*'aab'} == {'a', 'b'}, 'unpack string into set'\n# Heap-allocated set: covers the HeapData::Set arm in set_extend\ninner_set = {1, 2, 3}\nassert {*inner_set} == {1, 2, 3}, 'unpack set into set'\n# Heap-allocated Str (result of concat, not interned): covers HeapData::Str in set_extend\nhs = 'hel' + 'lo'\nassert {*hs} == {'h', 'e', 'l', 'o'}, 'unpack heap string into set'\n\n\n# Non-iterable heap-allocated Ref (closure) hits the inner `_` arm in set_extend.\n# A plain top-level function is Value::DefFunction (not a Ref), so a closure is\n# required to reach the Value::Ref(_) branch (HeapData that is not List/Tuple/Set/Dict/Str).\ndef _make_set_unpack_closure():\n    _sentinel = 1\n\n    def _inner():\n        return _sentinel\n\n    return _inner\n\n\n_set_unpack_closure = _make_set_unpack_closure()\ntry:\n    _x = {*_set_unpack_closure}\n    assert False, 'expected TypeError for non-iterable heap closure in set unpack'\nexcept TypeError:\n    pass\n"
  },
  {
    "path": "crates/monty/test_cases/set__review_bugs.py",
    "content": "# Tests for review issues\n\n# === frozenset repr for non-empty sets ===\n# frozenset repr should show \"frozenset({...})\" not just \"{...}\"\nfs_repr = repr(frozenset({1, 2}))\nassert fs_repr == 'frozenset({1, 2})' or fs_repr == 'frozenset({2, 1})', 'frozenset repr should include type name'\nassert repr(frozenset()) == 'frozenset()', 'empty frozenset repr'\n\n# set repr should NOT have type prefix\ns_repr = repr({1, 2})\nassert s_repr == '{1, 2}' or s_repr == '{2, 1}', 'set repr should not have prefix'\n\n# === issubset with range (non-Ref iterable) ===\n# These should work, not raise TypeError\ns = {1, 2, 3}\nassert s.issubset(range(10)), 'issubset should accept range'\nassert s.issuperset(range(1, 3)), 'issuperset should accept range'\nassert s.isdisjoint(range(10, 20)), 'isdisjoint should accept range'\n\n# === set construction with nested heap objects ===\n# This tests ref counting - if refs are dropped before incrementing, this will fail\nt = (1, 2)\ns = set([t])\nassert len(s) == 1, 'set should have one element'\nassert repr(s) == '{(1, 2)}', 'set repr should not have prefix'\n\n# More complex case - the list is temporary and will be dropped\ns2 = set([(3, 4)])\nassert len(s2) == 1, 'set from temp list should have one element'\nassert repr(s2) == '{(3, 4)}', 'set repr should not have prefix'\n\n# frozenset with nested objects\nfs = frozenset([(5, 6)])\nassert len(fs) == 1, 'frozenset from temp list should have one element'\nassert repr(fs) == 'frozenset({(5, 6)})', 'frozenset repr should not have prefix'\n"
  },
  {
    "path": "crates/monty/test_cases/set__unpack_type_error.py",
    "content": "{*42}\n# Raise=TypeError(\"'int' object is not iterable\")\n"
  },
  {
    "path": "crates/monty/test_cases/slice__invalid_indices.py",
    "content": "[1, 2, 3]['a':'b']\n# Raise=TypeError('slice indices must be integers or None or have an __index__ method')\n"
  },
  {
    "path": "crates/monty/test_cases/slice__kwargs.py",
    "content": "slice(stop=5)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__kwargs.py\", line 1, in <module>\n    slice(stop=5)\n    ~~~~~~~~~~~~~\nTypeError: slice() takes no keyword arguments\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__no_args.py",
    "content": "slice()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__no_args.py\", line 1, in <module>\n    slice()\n    ~~~~~~~\nTypeError: slice expected at least 1 argument, got 0\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__ops.py",
    "content": "# === Basic list slicing ===\nlst = [0, 1, 2, 3, 4, 5]\nassert lst[1:4] == [1, 2, 3], 'basic list slice'\nassert lst[:3] == [0, 1, 2], 'list slice from start'\nassert lst[3:] == [3, 4, 5], 'list slice to end'\nassert lst[:] == [0, 1, 2, 3, 4, 5], 'list full slice'\n\n# === Negative indices ===\nassert lst[-3:] == [3, 4, 5], 'list slice negative start'\nassert lst[:-2] == [0, 1, 2, 3], 'list slice negative stop'\nassert lst[-4:-1] == [2, 3, 4], 'list slice both negative'\n\n# === Step ===\nassert lst[::2] == [0, 2, 4], 'list slice with step'\nassert lst[1::2] == [1, 3, 5], 'list slice with start and step'\nassert lst[::-1] == [5, 4, 3, 2, 1, 0], 'list reverse slice'\nassert lst[4:1:-1] == [4, 3, 2], 'list negative step with bounds'\nassert lst[::3] == [0, 3], 'list slice step of 3'\n\n# === Out of bounds (clamped) ===\nassert lst[10:20] == [], 'list out of bounds high'\nassert lst[-100:2] == [0, 1], 'list out of bounds low'\nassert lst[2:100] == [2, 3, 4, 5], 'list stop beyond length'\n\n# === Empty results ===\nassert lst[3:1] == [], 'list empty slice start > stop'\nassert lst[3:3] == [], 'list empty slice start == stop'\n\n# === String slicing ===\ns = 'hello'\nassert s[1:4] == 'ell', 'string slice basic'\nassert s[:3] == 'hel', 'string slice from start'\nassert s[3:] == 'lo', 'string slice to end'\nassert s[:] == 'hello', 'string full slice'\nassert s[::-1] == 'olleh', 'string reverse'\nassert s[::2] == 'hlo', 'string slice with step'\n\n# === Unicode string slicing ===\nu = 'cafe'\nassert u[1:3] == 'af', 'unicode slice basic'\nassert u[::-1] == 'efac', 'unicode reverse'\n\n# === Tuple slicing ===\nt = (0, 1, 2, 3, 4)\nassert t[1:4] == (1, 2, 3), 'tuple slice basic'\nassert t[::-1] == (4, 3, 2, 1, 0), 'tuple reverse'\nassert t[::2] == (0, 2, 4), 'tuple slice with step'\n\n# === Bytes slicing ===\nb = b'\\x00\\x01\\x02\\x03\\x04'\nassert b[1:4] == b'\\x01\\x02\\x03', 'bytes slice basic'\nassert b[::-1] == b'\\x04\\x03\\x02\\x01\\x00', 'bytes reverse'\nassert b[::2] == b'\\x00\\x02\\x04', 'bytes slice with step'\n\n# === Range slicing ===\nr = range(10)\nassert r[2:5] == range(2, 5), 'range slice basic'\nassert r[::2] == range(0, 10, 2), 'range slice with step'\n\nr2 = range(0, 10, 2)\nassert r2[1:4] == range(2, 8, 2), 'stepped range slice'\n\n# === slice() builtin ===\ns1 = slice(3)\nassert s1.start is None, 'slice stop only - start is None'\nassert s1.stop == 3, 'slice stop only - stop is 3'\nassert s1.step is None, 'slice stop only - step is None'\n\ns2 = slice(1, 4)\nassert s2.start == 1, 'slice start stop - start is 1'\nassert s2.stop == 4, 'slice start stop - stop is 4'\nassert s2.step is None, 'slice start stop - step is None'\n\ns3 = slice(1, 10, 2)\nassert s3.start == 1, 'slice full - start is 1'\nassert s3.stop == 10, 'slice full - stop is 10'\nassert s3.step == 2, 'slice full - step is 2'\n\n# === Using slice objects ===\nsl = slice(1, 4)\nassert lst[sl] == [1, 2, 3], 'slice object for list'\nassert s[sl] == 'ell', 'slice object for string'\nassert t[sl] == (1, 2, 3), 'slice object for tuple'\n\n# === slice repr and str ===\nassert repr(slice(3)) == 'slice(None, 3, None)', 'slice repr stop only'\nassert repr(slice(1, 4)) == 'slice(1, 4, None)', 'slice repr start stop'\nassert repr(slice(1, 10, 2)) == 'slice(1, 10, 2)', 'slice repr full'\nassert str(slice(1, 4)) == 'slice(1, 4, None)', 'slice str same as repr'\n\n# === Edge case: negative step with None bounds ===\nassert lst[::-2] == [5, 3, 1], 'list negative step no bounds'\nassert s[::-2] == 'olh', 'string negative step no bounds'\n\n# === Edge case: step larger than length ===\nassert lst[::10] == [0], 'step larger than length'\n\n# === Empty sequence slicing ===\nempty_list = []\nassert empty_list[:] == [], 'empty list full slice'\nassert empty_list[1:4] == [], 'empty list any slice'\nassert empty_list[::-1] == [], 'empty list reverse'\n\nempty_str = ''\nassert empty_str[:] == '', 'empty string full slice'\nassert empty_str[1:4] == '', 'empty string any slice'\n\n# === Boolean truthiness of slice ===\nassert slice(1, 2), 'slice is truthy'\nassert slice(None), 'slice with None stop is truthy'\n\n# === Slice equality ===\nassert slice(1, 2) == slice(1, 2), 'slice equality same values'\nassert not (slice(1, 2) == slice(1, 3)), 'slice inequality different stop'\nassert slice(None) == slice(None), 'slice equality both None'\nassert slice(1, 2, 3) == slice(1, 2, 3), 'slice equality with step'\nassert not (slice(1, 2, 3) == slice(1, 2, 4)), 'slice inequality different step'\n\n# === Slice with bool indices ===\nassert [0, 1, 2, 3][True:] == [1, 2, 3], 'slice with True start'\nassert [0, 1, 2, 3][:True] == [0], 'slice with True stop'\nassert [0, 1, 2, 3][::True] == [0, 1, 2, 3], 'slice with True step'\nassert [0, 1, 2, 3][False:] == [0, 1, 2, 3], 'slice with False start'\nassert [0, 1, 2, 3][:False] == [], 'slice with False stop'\n\n# === Range slicing edge cases ===\nassert range(0)[1:2] == range(0, 0), 'empty range slicing'\nassert range(5)[::-1] == range(4, -1, -1), 'range reverse slice'\nassert list(range(5)[::-1]) == [4, 3, 2, 1, 0], 'range reverse slice iteration'\n\n# === Negative step with out-of-bounds start ===\nlst5 = [0, 1, 2, 3, 4]\nassert lst5[-10::-1] == [], 'far negative start with negative step should be empty'\nassert lst5[-6::-1] == [], 'just out of bounds negative start'\nassert lst5[-5::-1] == [0], 'exactly at first element'\nassert lst5[-4::-1] == [1, 0], 'second element backwards'\n\n# Range slicing with out-of-bounds negative start\nassert list(range(5)[-10::-1]) == [], 'range far negative start'\nassert list(range(5)[-6::-1]) == [], 'range just out of bounds'\nassert list(range(5)[-5::-1]) == [0], 'range exactly at first'\n\n# String slicing with out-of-bounds negative start\nassert 'hello'[-10::-1] == '', 'string far negative start empty'\nassert 'hello'[-5::-1] == 'h', 'string exactly at first'\n\n# Tuple slicing with out-of-bounds negative start\nassert (0, 1, 2, 3, 4)[-10::-1] == (), 'tuple far negative start empty'\nassert (0, 1, 2, 3, 4)[-5::-1] == (0,), 'tuple exactly at first'\n"
  },
  {
    "path": "crates/monty/test_cases/slice__step_zero.py",
    "content": "[1, 2, 3][::0]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__step_zero.py\", line 1, in <module>\n    [1, 2, 3][::0]\n    ~~~~~~~~~~~~~~\nValueError: slice step cannot be zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__step_zero_bytes.py",
    "content": "b'hello'[::0]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__step_zero_bytes.py\", line 1, in <module>\n    b'hello'[::0]\n    ~~~~~~~~~~~~~\nValueError: slice step cannot be zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__step_zero_range.py",
    "content": "range(5)[::0]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__step_zero_range.py\", line 1, in <module>\n    range(5)[::0]\n    ~~~~~~~~~~~~~\nValueError: slice step cannot be zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__step_zero_str.py",
    "content": "'hello'[::0]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__step_zero_str.py\", line 1, in <module>\n    'hello'[::0]\n    ~~~~~~~~~~~~\nValueError: slice step cannot be zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__step_zero_tuple.py",
    "content": "(1, 2, 3)[::0]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__step_zero_tuple.py\", line 1, in <module>\n    (1, 2, 3)[::0]\n    ~~~~~~~~~~~~~~\nValueError: slice step cannot be zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/slice__too_many_args.py",
    "content": "slice(1, 2, 3, 4)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"slice__too_many_args.py\", line 1, in <module>\n    slice(1, 2, 3, 4)\n    ~~~~~~~~~~~~~~~~~\nTypeError: slice expected at most 3 arguments, got 4\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__getitem_index_error.py",
    "content": "s = 'hello'\ns[10]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__getitem_index_error.py\", line 2, in <module>\n    s[10]\n    ~~~~~\nIndexError: string index out of range\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__index_not_found.py",
    "content": "'hello'.index('x')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__index_not_found.py\", line 1, in <module>\n    'hello'.index('x')\n    ~~~~~~~~~~~~~~~~~~\nValueError: substring not found\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__join_no_args.py",
    "content": "','.join()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__join_no_args.py\", line 1, in <module>\n    ','.join()\n    ~~~~~~~~~~\nTypeError: str.join() takes exactly one argument (0 given)\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__join_non_string.py",
    "content": "','.join([1, 2])\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__join_non_string.py\", line 1, in <module>\n    ','.join([1, 2])\n    ~~~~~~~~~~~~~~~~\nTypeError: sequence item 0: expected str instance, int found\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__join_not_iterable.py",
    "content": "','.join(123)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__join_not_iterable.py\", line 1, in <module>\n    ','.join(123)\n    ~~~~~~~~~~~~~\nTypeError: can only join an iterable\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__join_too_many_args.py",
    "content": "','.join(['a'], ['b'])\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__join_too_many_args.py\", line 1, in <module>\n    ','.join(['a'], ['b'])\n    ~~~~~~~~~~~~~~~~~~~~~~\nTypeError: str.join() takes exactly one argument (2 given)\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__methods.py",
    "content": "# === Phase 1: Simple transformations ===\n\n# lower()\nassert 'HELLO'.lower() == 'hello', 'lower basic'\nassert 'Hello World'.lower() == 'hello world', 'lower mixed'\nassert 'hello'.lower() == 'hello', 'lower already lower'\nassert ''.lower() == '', 'lower empty'\nassert '123'.lower() == '123', 'lower numbers unchanged'\n\n# upper()\nassert 'hello'.upper() == 'HELLO', 'upper basic'\nassert 'Hello World'.upper() == 'HELLO WORLD', 'upper mixed'\nassert 'HELLO'.upper() == 'HELLO', 'upper already upper'\nassert ''.upper() == '', 'upper empty'\nassert '123'.upper() == '123', 'upper numbers unchanged'\n\n# capitalize()\nassert 'hello'.capitalize() == 'Hello', 'capitalize basic'\nassert 'HELLO'.capitalize() == 'Hello', 'capitalize all upper'\nassert 'hELLO wORLD'.capitalize() == 'Hello world', 'capitalize mixed'\nassert ''.capitalize() == '', 'capitalize empty'\nassert '123abc'.capitalize() == '123abc', 'capitalize number start'\n\n# title()\nassert 'hello world'.title() == 'Hello World', 'title basic'\nassert 'HELLO WORLD'.title() == 'Hello World', 'title all upper'\nassert \"they're\".title() == \"They'Re\", 'title apostrophe'\nassert ''.title() == '', 'title empty'\nassert '123 abc'.title() == '123 Abc', 'title number start'\n\n# swapcase()\nassert 'Hello World'.swapcase() == 'hELLO wORLD', 'swapcase basic'\nassert 'HELLO'.swapcase() == 'hello', 'swapcase all upper'\nassert 'hello'.swapcase() == 'HELLO', 'swapcase all lower'\nassert ''.swapcase() == '', 'swapcase empty'\n\n# casefold()\nassert 'Hello'.casefold() == 'hello', 'casefold basic'\nassert 'HELLO'.casefold() == 'hello', 'casefold all upper'\nassert ''.casefold() == '', 'casefold empty'\n\n# === Phase 2: Predicate methods ===\n\n# isalpha()\nassert 'hello'.isalpha() == True, 'isalpha basic'\nassert 'Hello'.isalpha() == True, 'isalpha mixed case'\nassert ''.isalpha() == False, 'isalpha empty'\nassert 'hello123'.isalpha() == False, 'isalpha with digits'\nassert 'hello world'.isalpha() == False, 'isalpha with space'\n\n# isdigit()\nassert '123'.isdigit() == True, 'isdigit basic'\nassert ''.isdigit() == False, 'isdigit empty'\nassert '123abc'.isdigit() == False, 'isdigit with letters'\nassert '12 34'.isdigit() == False, 'isdigit with space'\n\n# isalnum()\nassert 'hello123'.isalnum() == True, 'isalnum basic'\nassert 'hello'.isalnum() == True, 'isalnum letters only'\nassert '123'.isalnum() == True, 'isalnum digits only'\nassert ''.isalnum() == False, 'isalnum empty'\nassert 'hello 123'.isalnum() == False, 'isalnum with space'\n\n# isnumeric()\nassert '123'.isnumeric() == True, 'isnumeric basic'\nassert ''.isnumeric() == False, 'isnumeric empty'\nassert '123abc'.isnumeric() == False, 'isnumeric with letters'\n\n# isspace()\nassert '   '.isspace() == True, 'isspace spaces'\nassert '\\t\\n'.isspace() == True, 'isspace tabs and newlines'\nassert ''.isspace() == False, 'isspace empty'\nassert ' a '.isspace() == False, 'isspace with letter'\n\n# islower()\nassert 'hello'.islower() == True, 'islower basic'\nassert 'Hello'.islower() == False, 'islower mixed'\nassert ''.islower() == False, 'islower empty'\nassert '123'.islower() == False, 'islower numbers only'\nassert 'hello123'.islower() == True, 'islower with numbers'\n\n# isupper()\nassert 'HELLO'.isupper() == True, 'isupper basic'\nassert 'Hello'.isupper() == False, 'isupper mixed'\nassert ''.isupper() == False, 'isupper empty'\nassert '123'.isupper() == False, 'isupper numbers only'\nassert 'HELLO123'.isupper() == True, 'isupper with numbers'\n\n# isascii()\nassert 'hello'.isascii() == True, 'isascii basic'\nassert ''.isascii() == True, 'isascii empty'\nassert '\\x00\\x7f'.isascii() == True, 'isascii boundary'\n\n# isdecimal()\nassert '123'.isdecimal() == True, 'isdecimal basic'\nassert ''.isdecimal() == False, 'isdecimal empty'\nassert '123abc'.isdecimal() == False, 'isdecimal with letters'\n\n# === Phase 3: Search methods ===\n\n# find()\nassert 'hello'.find('l') == 2, 'find basic'\nassert 'hello'.find('ll') == 2, 'find substring'\nassert 'hello'.find('x') == -1, 'find not found'\nassert 'hello'.find('') == 0, 'find empty string'\nassert 'hello'.find('l', 3) == 3, 'find with start'\nassert 'hello'.find('l', 0, 3) == 2, 'find with start and end'\n\n# rfind()\nassert 'hello'.rfind('l') == 3, 'rfind basic'\nassert 'hello'.rfind('x') == -1, 'rfind not found'\nassert 'hello'.rfind('l', 0, 3) == 2, 'rfind with end'\n\n# index()\nassert 'hello'.index('l') == 2, 'index basic'\nassert 'hello'.index('ll') == 2, 'index substring'\n\n# rindex()\nassert 'hello'.rindex('l') == 3, 'rindex basic'\n\n# count()\nassert 'hello'.count('l') == 2, 'count basic'\nassert 'hello'.count('ll') == 1, 'count substring'\nassert 'hello'.count('x') == 0, 'count not found'\nassert 'hello'.count('') == 6, 'count empty string'\nassert 'aaa'.count('a') == 3, 'count repeated'\n\n# startswith()\nassert 'hello'.startswith('he') == True, 'startswith basic'\nassert 'hello'.startswith('lo') == False, 'startswith false'\nassert 'hello'.startswith('') == True, 'startswith empty'\nassert 'hello'.startswith('ell', 1) == True, 'startswith with start'\n\n# endswith()\nassert 'hello'.endswith('lo') == True, 'endswith basic'\nassert 'hello'.endswith('he') == False, 'endswith false'\nassert 'hello'.endswith('') == True, 'endswith empty'\nassert 'hello'.endswith('ell', 0, 4) == True, 'endswith with end'\n\n# === Phase 4: Strip/trim methods ===\n\n# strip()\nassert '  hello  '.strip() == 'hello', 'strip whitespace'\nassert 'xxhelloxx'.strip('x') == 'hello', 'strip chars'\nassert 'hello'.strip() == 'hello', 'strip nothing'\nassert ''.strip() == '', 'strip empty'\nassert '   '.strip() == '', 'strip only whitespace'\n\n# lstrip()\nassert '  hello  '.lstrip() == 'hello  ', 'lstrip whitespace'\nassert 'xxhello'.lstrip('x') == 'hello', 'lstrip chars'\nassert 'hello'.lstrip() == 'hello', 'lstrip nothing'\n\n# rstrip()\nassert '  hello  '.rstrip() == '  hello', 'rstrip whitespace'\nassert 'helloxx'.rstrip('x') == 'hello', 'rstrip chars'\nassert 'hello'.rstrip() == 'hello', 'rstrip nothing'\n\n# removeprefix()\nassert 'hello world'.removeprefix('hello ') == 'world', 'removeprefix basic'\nassert 'hello world'.removeprefix('world') == 'hello world', 'removeprefix not found'\nassert 'hello'.removeprefix('') == 'hello', 'removeprefix empty'\n\n# removesuffix()\nassert 'hello world'.removesuffix(' world') == 'hello', 'removesuffix basic'\nassert 'hello world'.removesuffix('hello') == 'hello world', 'removesuffix not found'\nassert 'hello'.removesuffix('') == 'hello', 'removesuffix empty'\n\n# === Phase 5: Split methods ===\n\n# split()\nassert 'a b c'.split() == ['a', 'b', 'c'], 'split whitespace'\nassert 'a,b,c'.split(',') == ['a', 'b', 'c'], 'split comma'\nassert 'a,b,c'.split(',', 1) == ['a', 'b,c'], 'split maxsplit'\nassert '  a  b  '.split() == ['a', 'b'], 'split multiple spaces'\nassert 'hello'.split('x') == ['hello'], 'split not found'\n\n# rsplit()\nassert 'a b c'.rsplit() == ['a', 'b', 'c'], 'rsplit whitespace'\nassert 'a,b,c'.rsplit(',') == ['a', 'b', 'c'], 'rsplit comma'\nassert 'a,b,c'.rsplit(',', 1) == ['a,b', 'c'], 'rsplit maxsplit'\n\n# splitlines()\nassert 'a\\nb\\nc'.splitlines() == ['a', 'b', 'c'], 'splitlines basic'\nassert 'a\\nb\\nc'.splitlines(True) == ['a\\n', 'b\\n', 'c'], 'splitlines keepends'\nassert 'a\\r\\nb'.splitlines() == ['a', 'b'], 'splitlines crlf'\nassert ''.splitlines() == [], 'splitlines empty'\n\n# partition()\nassert 'hello world'.partition(' ') == ('hello', ' ', 'world'), 'partition basic'\nassert 'hello'.partition('x') == ('hello', '', ''), 'partition not found'\nassert 'hello world test'.partition(' ') == ('hello', ' ', 'world test'), 'partition first'\n\n# rpartition()\nassert 'hello world'.rpartition(' ') == ('hello', ' ', 'world'), 'rpartition basic'\nassert 'hello'.rpartition('x') == ('', '', 'hello'), 'rpartition not found'\nassert 'hello world test'.rpartition(' ') == ('hello world', ' ', 'test'), 'rpartition last'\n\n# === Phase 6: Replace/modify methods ===\n\n# replace()\nassert 'hello'.replace('l', 'L') == 'heLLo', 'replace basic'\nassert 'hello'.replace('l', 'L', 1) == 'heLlo', 'replace count'\nassert 'hello'.replace('x', 'y') == 'hello', 'replace not found'\nassert 'aaa'.replace('a', 'b') == 'bbb', 'replace all'\nassert ''.replace('a', 'b') == '', 'replace empty'\n\n# center()\nassert 'hi'.center(6) == '  hi  ', 'center basic'\nassert 'hi'.center(6, '-') == '--hi--', 'center fillchar'\nassert 'hi'.center(2) == 'hi', 'center no padding'\nassert 'hi'.center(1) == 'hi', 'center smaller'\n\n# ljust()\nassert 'hi'.ljust(6) == 'hi    ', 'ljust basic'\nassert 'hi'.ljust(6, '-') == 'hi----', 'ljust fillchar'\nassert 'hi'.ljust(2) == 'hi', 'ljust no padding'\n\n# rjust()\nassert 'hi'.rjust(6) == '    hi', 'rjust basic'\nassert 'hi'.rjust(6, '-') == '----hi', 'rjust fillchar'\nassert 'hi'.rjust(2) == 'hi', 'rjust no padding'\n\n# zfill()\nassert '42'.zfill(5) == '00042', 'zfill basic'\nassert '-42'.zfill(5) == '-0042', 'zfill negative'\nassert '+42'.zfill(5) == '+0042', 'zfill positive'\nassert '42'.zfill(2) == '42', 'zfill no padding'\nassert ''.zfill(3) == '000', 'zfill empty'\n\n# === Phase 7: Additional tests for Python compatibility ===\n\n# startswith/endswith with tuple\nassert 'hello'.startswith(('he', 'lo')) == True, 'startswith tuple first match'\nassert 'hello'.startswith(('lo', 'he')) == True, 'startswith tuple second match'\nassert 'hello'.startswith(('x', 'y')) == False, 'startswith tuple no match'\nassert 'hello'.endswith(('he', 'lo')) == True, 'endswith tuple first match'\nassert 'hello'.endswith(('lo', 'he')) == True, 'endswith tuple second match'\nassert 'hello'.endswith(('x', 'y')) == False, 'endswith tuple no match'\nassert 'hello'.startswith(('ell',), 1) == True, 'startswith tuple with start'\n\n# find/rfind/index/rindex/count with None as start/end\nassert 'hello'.find('l', None) == 2, 'find with None start'\nassert 'hello'.find('l', None, None) == 2, 'find with None start and end'\nassert 'hello'.find('l', 0, None) == 2, 'find with None end'\nassert 'hello'.rfind('l', None, None) == 3, 'rfind with None start and end'\nassert 'hello'.count('l', None, None) == 2, 'count with None start and end'\nassert 'hello'.startswith('he', None) == True, 'startswith with None start'\nassert 'hello'.endswith('lo', None, None) == True, 'endswith with None start and end'\n\n# strip with None\nassert '  hello  '.strip(None) == 'hello', 'strip None same as no arg'\nassert '  hello  '.lstrip(None) == 'hello  ', 'lstrip None same as no arg'\nassert '  hello  '.rstrip(None) == '  hello', 'rstrip None same as no arg'\n\n# === Phase 8: Keyword argument tests ===\n\n# split with keyword args\nassert 'a,b,c'.split(sep=',') == ['a', 'b', 'c'], 'split sep kwarg'\nassert 'a,b,c'.split(',', maxsplit=1) == ['a', 'b,c'], 'split maxsplit kwarg'\nassert 'a,b,c'.split(sep=',', maxsplit=1) == ['a', 'b,c'], 'split both kwargs'\n\n# rsplit with keyword args\nassert 'a,b,c'.rsplit(sep=',') == ['a', 'b', 'c'], 'rsplit sep kwarg'\nassert 'a,b,c'.rsplit(',', maxsplit=1) == ['a,b', 'c'], 'rsplit maxsplit kwarg'\nassert 'a,b,c'.rsplit(sep=',', maxsplit=1) == ['a,b', 'c'], 'rsplit both kwargs'\n\n# splitlines with keyword args\nassert 'a\\nb\\nc'.splitlines(keepends=True) == ['a\\n', 'b\\n', 'c'], 'splitlines keepends kwarg'\nassert 'a\\nb\\nc'.splitlines(keepends=False) == ['a', 'b', 'c'], 'splitlines keepends=False'\n\n# replace with keyword args\nassert 'aaa'.replace('a', 'b', count=2) == 'bba', 'replace count kwarg'\n\n# === Phase 9: Additional methods ===\n\n# encode()\nassert 'hello'.encode() == b'hello', 'encode default'\nassert 'hello'.encode('utf-8') == b'hello', 'encode utf-8'\nassert 'hello'.encode('utf8') == b'hello', 'encode utf8 alias'\nassert 'hello'.encode('UTF-8') == b'hello', 'encode UTF-8 case insensitive'\nassert ''.encode() == b'', 'encode empty'\nassert 'hello'.encode('utf-8', 'strict') == b'hello', 'encode with errors'\n\n# isidentifier()\nassert 'hello'.isidentifier() == True, 'isidentifier basic'\nassert '_hello'.isidentifier() == True, 'isidentifier underscore'\nassert '__init__'.isidentifier() == True, 'isidentifier dunder'\nassert 'hello123'.isidentifier() == True, 'isidentifier with digits'\nassert ''.isidentifier() == False, 'isidentifier empty'\nassert '123hello'.isidentifier() == False, 'isidentifier digit start'\nassert 'hello world'.isidentifier() == False, 'isidentifier with space'\nassert 'hello-world'.isidentifier() == False, 'isidentifier with dash'\nassert 'class'.isidentifier() == True, 'isidentifier keyword'  # isidentifier doesn't check keywords\n\n# istitle()\nassert 'Hello World'.istitle() == True, 'istitle basic'\nassert 'Hello'.istitle() == True, 'istitle single word'\nassert 'HELLO'.istitle() == False, 'istitle all upper'\nassert 'hello'.istitle() == False, 'istitle all lower'\nassert ''.istitle() == False, 'istitle empty'\nassert 'Hello world'.istitle() == False, 'istitle lowercase word'\nassert '123'.istitle() == False, 'istitle numbers only'\nassert 'Hello 123 World'.istitle() == True, 'istitle with numbers'\nassert \"They'Re\".istitle() == True, 'istitle apostrophe'\n\n# === Phase 10: Unicode support for is* methods ===\n\n# isdecimal with Unicode decimal digits\nassert '٠١٢٣٤٥٦٧٨٩'.isdecimal() == True, 'isdecimal Arabic-Indic'\nassert '０１２３４５６７８９'.isdecimal() == True, 'isdecimal Fullwidth'\nassert '०१२३४५६७८९'.isdecimal() == True, 'isdecimal Devanagari'\nassert '²'.isdecimal() == False, 'isdecimal superscript not decimal'\nassert '½'.isdecimal() == False, 'isdecimal fraction not decimal'\n\n# isdigit with superscripts and subscripts\nassert '²³'.isdigit() == True, 'isdigit superscripts'\nassert '₀₁₂₃₄₅₆₇₈₉'.isdigit() == True, 'isdigit subscripts'\nassert '0123456789'.isdigit() == True, 'isdigit ASCII'\nassert '٠١٢٣٤٥٦٧٨٩'.isdigit() == True, 'isdigit Arabic-Indic'\nassert '½'.isdigit() == False, 'isdigit fraction not digit'\n\n# isnumeric with fractions and other numerics\nassert '½'.isnumeric() == True, 'isnumeric fraction'\nassert '²'.isnumeric() == True, 'isnumeric superscript'\nassert '٠١٢٣٤٥٦٧٨٩'.isnumeric() == True, 'isnumeric Arabic-Indic'\nassert '0123456789'.isnumeric() == True, 'isnumeric ASCII'\n"
  },
  {
    "path": "crates/monty/test_cases/str__ops.py",
    "content": "# === String concatenation (+) ===\nassert 'hello' + ' ' + 'world' == 'hello world', 'basic concat'\nassert '' + 'test' == 'test', 'empty left concat'\nassert 'test' + '' == 'test', 'empty right concat'\nassert '' + '' == '', 'empty both concat'\nassert 'a' + 'b' + 'c' + 'd' == 'abcd', 'multiple concat'\n\n# === Augmented assignment (+=) ===\ns = 'hello'\ns += ' world'\nassert s == 'hello world', 'basic iadd'\n\ns = 'test'\ns += ''\nassert s == 'test', 'iadd empty'\n\ns = 'a'\ns += 'b'\ns += 'c'\nassert s == 'abc', 'multiple iadd'\n\ns = 'ab'\ns += s\nassert s == 'abab', 'iadd self'\n\n# === String length ===\nassert len('') == 0, 'len empty'\nassert len('a') == 1, 'len single'\nassert len('hello') == 5, 'len basic'\nassert len('hello world') == 11, 'len with space'\nassert len('caf\\xe9') == 4, 'len unicode'\n\n# === String repr/str ===\nassert repr('') == \"''\", 'empty string repr'\nassert str('') == '', 'empty string str'\n\nassert repr('hello') == \"'hello'\", 'string repr'\nassert str('hello') == 'hello', 'string str'\n\nassert repr('hello \"world\"') == '\\'hello \"world\"\\'', 'string with quotes repr'\nassert str('hello \"world\"') == 'hello \"world\"', 'string with quotes str'\n\n# === String repetition (*) ===\nassert 'ab' * 3 == 'ababab', 'str mult int'\nassert 3 * 'ab' == 'ababab', 'int mult str'\nassert 'x' * 0 == '', 'str mult zero'\nassert 'x' * -1 == '', 'str mult negative'\nassert '' * 5 == '', 'empty str mult'\nassert 'a' * 1 == 'a', 'str mult one'\n\n# === String repetition augmented assignment (*=) ===\ns = 'ab'\ns *= 3\nassert s == 'ababab', 'str imult'\n\ns = 'x'\ns *= 0\nassert s == '', 'str imult zero'\n\n# === String join method ===\n# Basic join on literals\nassert ','.join(['a', 'b', 'c']) == 'a,b,c', 'join list with comma'\nassert ''.join(['a', 'b', 'c']) == 'abc', 'join with empty separator'\nassert '-'.join([]) == '', 'join empty list'\nassert ','.join(['only']) == 'only', 'join single element'\n\n# Join with different iterables\nassert ' '.join(('hello', 'world')) == 'hello world', 'join tuple'\n\n# Join with string iterable (iterates over characters)\nassert ','.join('abc') == 'a,b,c', 'join string iterable'\n\n# Join with variable separator\nsep = '-'\nassert sep.join(['a', 'b']) == 'a-b', 'join with variable separator'\n\n# Heap-allocated string separator\ns = str('.')\nassert s.join(['a', 'b']) == 'a.b', 'join with heap string'\n\n# Mixed string types in iterable (interned and heap)\nmixed = ['hello', str('world')]\nassert ' '.join(mixed) == 'hello world', 'join with mixed string types'\n\n# === String indexing (getitem) ===\n# Basic indexing\nassert 'hello'[0] == 'h', 'getitem index 0'\nassert 'hello'[1] == 'e', 'getitem index 1'\nassert 'hello'[4] == 'o', 'getitem last index'\n\n# Negative indexing\nassert 'hello'[-1] == 'o', 'getitem -1'\nassert 'hello'[-2] == 'l', 'getitem -2'\nassert 'hello'[-5] == 'h', 'getitem -5'\n\n# Single character strings\nassert 'a'[0] == 'a', 'getitem single char at 0'\nassert 'a'[-1] == 'a', 'getitem single char at -1'\n\n# Unicode strings\ns = 'café'\nassert s[0] == 'c', 'unicode getitem 0'\nassert s[1] == 'a', 'unicode getitem 1'\nassert s[2] == 'f', 'unicode getitem 2'\nassert s[3] == 'é', 'unicode getitem 3 (accented)'\nassert s[-1] == 'é', 'unicode getitem -1'\n\n# Multi-byte unicode (CJK characters)\ns = '日本語'\nassert s[0] == '日', 'cjk getitem 0'\nassert s[1] == '本', 'cjk getitem 1'\nassert s[2] == '語', 'cjk getitem 2'\nassert s[-1] == '語', 'cjk getitem -1'\n\n# Emoji (multi-byte UTF-8)\ns = 'a🎉b'\nassert s[0] == 'a', 'emoji string getitem 0'\nassert s[1] == '🎉', 'emoji string getitem 1 (emoji)'\nassert s[2] == 'b', 'emoji string getitem 2'\n\n# Heap-allocated strings\ns = str('hello')\nassert s[0] == 'h', 'heap string getitem'\nassert s[-1] == 'o', 'heap string negative getitem'\n\n# Variable index\ns = 'abc'\ni = 1\nassert s[i] == 'b', 'getitem with variable index'\n\n# Bool indices (True=1, False=0)\ns = 'abc'\nassert s[False] == 'a', 'str getitem with False'\nassert s[True] == 'b', 'str getitem with True'\n\n# === Sorting and comparisons ===\nassert 'a' < 'b', 'str < str'\nassert 'b' > 'a', 'str > str'\nassert 'a' <= 'a', 'str <= str equal'\nassert 'a' <= 'b', 'str <= str less'\nassert 'b' >= 'b', 'str >= str equal'\nassert 'b' >= 'a', 'str >= str greater'\nassert not ('b' < 'a'), 'str not < str'\nassert not ('a' > 'b'), 'str not > str'\n\n# Different lengths\nassert 'a' < 'aa', 'shorter prefix is less'\nassert 'ab' < 'b', 'first char decides'\nassert '' < 'a', 'empty string is less'\nassert 'abc' > 'ab', 'longer string with same prefix is greater'\n\n# Non-ASCII comparisons (by Unicode code point)\nassert 'café' < 'cafë', 'non-ascii comparison (é < ë)'\nassert 'z' < 'é', 'ascii < non-ascii (z < é)'\nassert '日' < '本', 'CJK comparison by code point'\nassert '😀' < '😁', 'emoji comparison by code point'\n\n# Sorting\nassert sorted('cba') == ['a', 'b', 'c'], 'sorted string'\nassert sorted(['b', 'c', 'a']) == ['a', 'b', 'c'], 'sorted list of strings'\nassert sorted(['café', 'cafë', 'cafa']) == ['cafa', 'café', 'cafë'], 'sorted non-ascii strings'\nassert sorted(['bb', 'a', 'ba']) == ['a', 'ba', 'bb'], 'sorted different length strings'\n"
  },
  {
    "path": "crates/monty/test_cases/str__partition_empty.py",
    "content": "'hello'.partition('')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__partition_empty.py\", line 1, in <module>\n    'hello'.partition('')\n    ~~~~~~~~~~~~~~~~~~~~~\nValueError: empty separator\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__rsplit_empty_sep.py",
    "content": "'hello'.rsplit('')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__rsplit_empty_sep.py\", line 1, in <module>\n    'hello'.rsplit('')\n    ~~~~~~~~~~~~~~~~~~\nValueError: empty separator\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/str__split_empty_sep.py",
    "content": "'hello'.split('')\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"str__split_empty_sep.py\", line 1, in <module>\n    'hello'.split('')\n    ~~~~~~~~~~~~~~~~~\nValueError: empty separator\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/sys__types.py",
    "content": "# Tests for sys module types\n\nimport sys\n\n# === Verify type() returns _io.TextIOWrapper for stdout/stderr ===\nassert str(type(sys.stdout)) == \"<class '_io.TextIOWrapper'>\", 'type(stdout) is _io.TextIOWrapper'\nassert str(type(sys.stderr)) == \"<class '_io.TextIOWrapper'>\", 'type(stderr) is _io.TextIOWrapper'\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__division_error.py",
    "content": "def foo():\n    1 / 0\n\n\ndef bar():\n    foo()\n\n\ndef baz():\n    bar()\n\n\nbaz()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__division_error.py\", line 13, in <module>\n    baz()\n    ~~~~~\n  File \"traceback__division_error.py\", line 10, in baz\n    bar()\n    ~~~~~\n  File \"traceback__division_error.py\", line 6, in bar\n    foo()\n    ~~~~~\n  File \"traceback__division_error.py\", line 2, in foo\n    1 / 0\n    ~~~~~\nZeroDivisionError: division by zero\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__index_error.py",
    "content": "def foo():\n    a = []\n    a[1]\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__index_error.py\", line 6, in <module>\n    foo()\n    ~~~~~\n  File \"traceback__index_error.py\", line 3, in foo\n    a[1]\n    ~~~~\nIndexError: list index out of range\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__insert_as_int.py",
    "content": "a = []\na.insert({1: 2}, 2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__insert_as_int.py\", line 2, in <module>\n    a.insert({1: 2}, 2)\n    ~~~~~~~~~~~~~~~~~~~\nTypeError: 'dict' object cannot be interpreted as an integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__nested_call.py",
    "content": "def foo():\n    raise ValueError('xxx')\n\n\ndef bar():\n    foo()\n\n\ndef baz():\n    bar()\n\n\nbaz()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__nested_call.py\", line 13, in <module>\n    baz()\n    ~~~~~\n  File \"traceback__nested_call.py\", line 10, in baz\n    bar()\n    ~~~~~\n  File \"traceback__nested_call.py\", line 6, in bar\n    foo()\n    ~~~~~\n  File \"traceback__nested_call.py\", line 2, in foo\n    raise ValueError('xxx')\nValueError: xxx\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__nonlocal_module_scope.py",
    "content": "# nonlocal at module level is a syntax error\nnonlocal x  # type: ignore\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__nonlocal_module_scope.py\", line 2\n    nonlocal x  # type: ignore\n    ~~~~~~~~~~\nSyntaxError: nonlocal declaration not allowed at module level\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__nonlocal_unbound.py",
    "content": "def outer():\n    def inner():\n        nonlocal x\n        return x\n\n    inner()\n    x = 1\n\n\nouter()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__nonlocal_unbound.py\", line 10, in <module>\n    outer()\n    ~~~~~~~\n  File \"traceback__nonlocal_unbound.py\", line 6, in outer\n    inner()\n    ~~~~~~~\n  File \"traceback__nonlocal_unbound.py\", line 4, in inner\n    return x\n           ~\nNameError: cannot access free variable 'x' where it is not associated with a value in enclosing scope\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__range_as_int.py",
    "content": "range([1])\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__range_as_int.py\", line 1, in <module>\n    range([1])\n    ~~~~~~~~~~\nTypeError: 'list' object cannot be interpreted as an integer\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__recursion_error.py",
    "content": "def recurse():\n    recurse()\n\n\nrecurse()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__recursion_error.py\", line 5, in <module>\n    recurse()\n    ~~~~~~~~~\n  File \"traceback__recursion_error.py\", line 2, in recurse\n    recurse()\n    ~~~~~~~~~\n  File \"traceback__recursion_error.py\", line 2, in recurse\n    recurse()\n    ~~~~~~~~~\n  File \"traceback__recursion_error.py\", line 2, in recurse\n    recurse()\n    ~~~~~~~~~\n  [Previous line repeated 47 more times]\nRecursionError: maximum recursion depth exceeded\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__set_mutation.py",
    "content": "s = {1, 2}\nfor x in s:\n    s.add(3)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__set_mutation.py\", line 2, in <module>\n    for x in s:\n             ~\nRuntimeError: Set changed size during iteration\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__undefined_attr_call.py",
    "content": "def foo():\n    snap.method()\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__undefined_attr_call.py\", line 5, in <module>\n    foo()\n    ~~~~~\n  File \"traceback__undefined_attr_call.py\", line 2, in foo\n    snap.method()\n    ~~~~\nNameError: name 'snap' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__undefined_call.py",
    "content": "def foo():\n    snap(1)\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__undefined_call.py\", line 5, in <module>\n    foo()\n    ~~~~~\n  File \"traceback__undefined_call.py\", line 2, in foo\n    snap(1)\n    ~~~~\nNameError: name 'snap' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/traceback__undefined_raise.py",
    "content": "def foo():\n    raise snap\n\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"traceback__undefined_raise.py\", line 5, in <module>\n    foo()\n    ~~~~~\n  File \"traceback__undefined_raise.py\", line 2, in foo\n    raise snap\n          ~~~~\nNameError: name 'snap' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/try_except__all.py",
    "content": "# === Basic exception catching ===\ncaught = False\ntry:\n    raise ValueError('test')\nexcept ValueError:\n    caught = True\nassert caught, 'should catch ValueError'\n\n# === Exception variable binding ===\nmsg = None\ntry:\n    raise TypeError('the message')\nexcept TypeError as e:\n    msg = repr(e)\n# repr(e) returns \"TypeError('the message')\" - confirms we caught the right exception\nassert msg == \"TypeError('the message')\", 'should capture exception'\n\n# === Multiple handlers - first match wins ===\nwhich = None\ntry:\n    raise TypeError('type error')\nexcept ValueError:\n    which = 'value'\nexcept TypeError:\n    which = 'type'\nexcept:\n    which = 'bare'\nassert which == 'type', 'first matching handler should be used'\n\n# === Bare except catches all ===\ncaught_bare = False\ntry:\n    raise KeyError('key')\nexcept:\n    caught_bare = True\nassert caught_bare, 'bare except should catch all'\n\n# === Else block runs when no exception ===\nelse_ran = False\ntry:\n    x = 1\nexcept:\n    pass\nelse:\n    else_ran = True\nassert else_ran, 'else should run when no exception'\n\n# === Else block does not run when exception occurs ===\nelse_ran_with_exc = True\ntry:\n    raise ValueError()\nexcept ValueError:\n    pass\nelse:\n    else_ran_with_exc = False\nassert else_ran_with_exc, 'else should not run when exception occurs'\n\n# === Finally always runs after try ===\nfinally_ran = False\ntry:\n    x = 1\nfinally:\n    finally_ran = True\nassert finally_ran, 'finally should run after try'\n\n# === Finally runs after exception caught ===\nfinally_after_catch = False\ntry:\n    raise ValueError()\nexcept ValueError:\n    pass\nfinally:\n    finally_after_catch = True\nassert finally_after_catch, 'finally should run after exception caught'\n\n# === Bare raise re-raises current exception ===\ncaught_reraised = False\ntry:\n    try:\n        raise ValueError('original')\n    except ValueError:\n        raise  # bare raise\nexcept ValueError as e:\n    caught_reraised = repr(e) == \"ValueError('original')\"\nassert caught_reraised, 'bare raise should re-raise original exception'\n\n# === Nested try/except ===\nouter_caught = False\ninner_caught = False\ntry:\n    try:\n        raise ValueError('inner')\n    except ValueError:\n        inner_caught = True\n        raise TypeError('outer')\nexcept TypeError:\n    outer_caught = True\nassert inner_caught and outer_caught, 'nested exceptions should work'\n\n# === Exception base class matches all ===\ncaught_by_base = False\ntry:\n    raise KeyError('key')\nexcept Exception:\n    caught_by_base = True\nassert caught_by_base, 'Exception should catch all exception types'\n\n# === Tuple of exception types ===\ncaught_tuple = False\ntry:\n    raise TypeError('type')\nexcept (ValueError, TypeError):\n    caught_tuple = True\nassert caught_tuple, 'tuple of types should match'\n\n\n# === Return in try with finally ===\ndef try_return_finally():\n    try:\n        return 1\n    finally:\n        pass\n\n\nassert try_return_finally() == 1, 'return in try should work with finally'\n\n\n# === Return in finally overrides try return ===\ndef finally_return_overrides():\n    try:\n        return 1\n    finally:\n        return 2  # type: ignore[returnInFinally]\n\n\nassert finally_return_overrides() == 2, 'finally return should override try return'\n\n# === Exception in handler propagates ===\nhandler_exc_propagated = False\ntry:\n    try:\n        raise ValueError()\n    except ValueError:\n        raise TypeError('from handler')\nexcept TypeError as e:\n    handler_exc_propagated = repr(e) == \"TypeError('from handler')\"\nassert handler_exc_propagated, 'exception in handler should propagate'\n\n\n# === Return in finally overrides exception from handler ===\ndef finally_return_overrides_handler_exc():\n    try:\n        raise TypeError('Error')\n    finally:\n        return 'finally wins handler'  # type: ignore\n\n\nassert finally_return_overrides_handler_exc() == 'finally wins handler', (\n    'return in finally should override exception from handler'\n)\n\n\ndef finally_return_overrides_handler_exc2():\n    try:\n        try:\n            raise ValueError('inner')\n        except ValueError:\n            raise TypeError('handler failure')\n    finally:\n        return 'finally wins handler'  # type: ignore\n\n\nassert finally_return_overrides_handler_exc2() == 'finally wins handler', (\n    'return in finally should override exception from handler'\n)\n\n\n# === Return in finally overrides exception from else ===\ndef finally_return_overrides_else_exc():\n    try:\n        try:\n            pass\n        except ValueError:\n            pass\n        else:\n            raise RuntimeError('else failure')\n    finally:\n        return 'finally wins else'  # type: ignore\n\n\nassert finally_return_overrides_else_exc() == 'finally wins else', (\n    'return in finally should override exception from else block'\n)\n\n# === Exception variable is cleared after handler ===\n# After except handler, the exception variable is deleted (Python 3 behavior)\ne_cleared = False\ntry:\n    try:\n        raise ValueError('test')\n    except ValueError as e:\n        pass\n    # e should be undefined here in Python 3, accessing it raises NameError\n    _ = e  # This should raise NameError\nexcept NameError:\n    e_cleared = True\nassert e_cleared, 'exception variable should be deleted after handler'\n\n# === Unhandled exception propagates ===\nunhandled_propagated = False\ntry:\n    try:\n        raise KeyError('unhandled')\n    except ValueError:\n        pass  # KeyError doesn't match, should propagate\nexcept KeyError as e:\n    unhandled_propagated = repr(e) == \"KeyError('unhandled')\"\nassert unhandled_propagated, 'unhandled exception should propagate to outer try'\n\n# === Finally runs before unhandled exception propagates ===\nfinally_before_propagate = False\ntry:\n    try:\n        raise KeyError('propagate')\n    except ValueError:\n        pass\n    finally:\n        finally_before_propagate = True\nexcept KeyError:\n    pass\nassert finally_before_propagate, 'finally should run before exception propagates'\n\n# === Exception in finally replaces original exception ===\nfinally_exc_wins = False\ntry:\n    try:\n        raise ValueError('original')\n    finally:\n        raise TypeError('from finally')\nexcept TypeError as e:\n    finally_exc_wins = repr(e) == \"TypeError('from finally')\"\nexcept ValueError:\n    finally_exc_wins = False  # Should not reach here\nassert finally_exc_wins, 'exception in finally should replace original'\n\n# === Exception in else propagates ===\nelse_exc_propagated = False\ntry:\n    try:\n        pass  # No exception in try\n    except:\n        pass\n    else:\n        raise ValueError('from else')\nexcept ValueError as e:\n    else_exc_propagated = repr(e) == \"ValueError('from else')\"\nassert else_exc_propagated, 'exception in else should propagate'\n\n# === Finally runs after exception in else ===\nfinally_after_else_exc = False\ntry:\n    try:\n        pass\n    except:\n        pass\n    else:\n        raise ValueError('else error')\n    finally:\n        finally_after_else_exc = True\nexcept ValueError:\n    pass\nassert finally_after_else_exc, 'finally should run after exception in else'\n\n# === Exception hierarchy: LookupError ===\n# LookupError should catch KeyError\ncaught_key_by_lookup = False\ntry:\n    raise KeyError('key')\nexcept LookupError:\n    caught_key_by_lookup = True\nassert caught_key_by_lookup, 'LookupError should catch KeyError'\n\n# LookupError should catch IndexError\ncaught_index_by_lookup = False\ntry:\n    raise IndexError('index')\nexcept LookupError:\n    caught_index_by_lookup = True\nassert caught_index_by_lookup, 'LookupError should catch IndexError'\n\n# LookupError should NOT catch ValueError\ncaught_value_by_lookup = False\ntry:\n    try:\n        raise ValueError('value')\n    except LookupError:\n        caught_value_by_lookup = True\nexcept ValueError:\n    pass\nassert not caught_value_by_lookup, 'LookupError should NOT catch ValueError'\n\n# === Exception hierarchy: ArithmeticError ===\n# ArithmeticError should catch ZeroDivisionError\ncaught_zero_by_arith = False\ntry:\n    raise ZeroDivisionError('zero')\nexcept ArithmeticError:\n    caught_zero_by_arith = True\nassert caught_zero_by_arith, 'ArithmeticError should catch ZeroDivisionError'\n\n# ArithmeticError should catch OverflowError\ncaught_overflow_by_arith = False\ntry:\n    raise OverflowError('overflow')\nexcept ArithmeticError:\n    caught_overflow_by_arith = True\nassert caught_overflow_by_arith, 'ArithmeticError should catch OverflowError'\n\n# === Exception hierarchy: RuntimeError ===\n# RuntimeError should catch NotImplementedError\ncaught_notimpl_by_runtime = False\ntry:\n    raise NotImplementedError('not impl')\nexcept RuntimeError:\n    caught_notimpl_by_runtime = True\nassert caught_notimpl_by_runtime, 'RuntimeError should catch NotImplementedError'\n\n# RuntimeError should catch RecursionError\ncaught_recursion_by_runtime = False\ntry:\n    raise RecursionError('recursion')\nexcept RuntimeError:\n    caught_recursion_by_runtime = True\nassert caught_recursion_by_runtime, 'RuntimeError should catch RecursionError'\n\n# === Exception hierarchy in tuple ===\n# Tuple containing base class should catch derived\ncaught_by_tuple_base = False\ntry:\n    raise KeyError('key')\nexcept (ValueError, LookupError):\n    caught_by_tuple_base = True\nassert caught_by_tuple_base, 'tuple with LookupError should catch KeyError'\n\n# === isinstance with exception hierarchy ===\ntry:\n    raise KeyError('key')\nexcept KeyError as e:\n    assert isinstance(e, KeyError), 'exception should be instance of KeyError'\n    assert isinstance(e, LookupError), 'KeyError should be instance of LookupError'\n    assert isinstance(e, Exception), 'KeyError should be instance of Exception'\n    assert not isinstance(e, ArithmeticError), 'KeyError should not be ArithmeticError'\n\ntry:\n    raise ZeroDivisionError('zero')\nexcept ZeroDivisionError as e:\n    assert isinstance(e, ZeroDivisionError), 'exception should be instance of ZeroDivisionError'\n    assert isinstance(e, ArithmeticError), 'ZeroDivisionError should be instance of ArithmeticError'\n    assert isinstance(e, Exception), 'ZeroDivisionError should be instance of Exception'\n    assert not isinstance(e, LookupError), 'ZeroDivisionError should not be LookupError'\n\n# === Multiple handlers where none match ===\n# Exception should propagate when no handler matches\nmulti_no_match_propagated = False\ntry:\n    try:\n        raise MemoryError('out of memory')\n    except ValueError:\n        pass\n    except TypeError:\n        pass\n    except KeyError:\n        pass\nexcept MemoryError as e:\n    multi_no_match_propagated = repr(e) == \"MemoryError('out of memory')\"\nassert multi_no_match_propagated, 'exception should propagate when no handler matches'\n\n# === BaseException hierarchy ===\n# BaseException should catch all exceptions including Exception subclasses\ncaught_value_by_base = False\ntry:\n    raise ValueError('value')\nexcept BaseException:\n    caught_value_by_base = True\nassert caught_value_by_base, 'BaseException should catch ValueError'\n\ncaught_key_by_base = False\ntry:\n    raise KeyError('key')\nexcept BaseException:\n    caught_key_by_base = True\nassert caught_key_by_base, 'BaseException should catch KeyError'\n\ncaught_type_by_base = False\ntry:\n    raise TypeError('type')\nexcept BaseException:\n    caught_type_by_base = True\nassert caught_type_by_base, 'BaseException should catch TypeError'\n\n# BaseException catches KeyboardInterrupt\ncaught_keyboard_by_base = False\ntry:\n    raise KeyboardInterrupt()\nexcept BaseException:\n    caught_keyboard_by_base = True\nassert caught_keyboard_by_base, 'BaseException should catch KeyboardInterrupt'\n\n# BaseException catches SystemExit\ncaught_sysexit_by_base = False\ntry:\n    raise SystemExit()\nexcept BaseException:\n    caught_sysexit_by_base = True\nassert caught_sysexit_by_base, 'BaseException should catch SystemExit'\n\n# === Exception does NOT catch BaseException direct subclasses ===\n# Exception should NOT catch KeyboardInterrupt\ncaught_keyboard_by_exc = False\ntry:\n    try:\n        raise KeyboardInterrupt()\n    except Exception:\n        caught_keyboard_by_exc = True\nexcept BaseException:\n    pass\nassert not caught_keyboard_by_exc, 'Exception should NOT catch KeyboardInterrupt'\n\n# Exception should NOT catch SystemExit\ncaught_sysexit_by_exc = False\ntry:\n    try:\n        raise SystemExit()\n    except Exception:\n        caught_sysexit_by_exc = True\nexcept BaseException:\n    pass\nassert not caught_sysexit_by_exc, 'Exception should NOT catch SystemExit'\n\n# But Exception SHOULD catch regular exceptions\ncaught_value_by_exc = False\ntry:\n    raise ValueError('test')\nexcept Exception:\n    caught_value_by_exc = True\nassert caught_value_by_exc, 'Exception should catch ValueError'\n\n# === isinstance with BaseException ===\ntry:\n    raise ValueError('test')\nexcept ValueError as e:\n    assert isinstance(e, BaseException), 'ValueError should be instance of BaseException'\n\ntry:\n    raise KeyboardInterrupt()\nexcept KeyboardInterrupt as e:\n    assert isinstance(e, BaseException), 'KeyboardInterrupt should be instance of BaseException'\n    assert not isinstance(e, Exception), 'KeyboardInterrupt should NOT be instance of Exception'\n\ntry:\n    raise SystemExit()\nexcept SystemExit as e:\n    assert isinstance(e, BaseException), 'SystemExit should be instance of BaseException'\n    assert not isinstance(e, Exception), 'SystemExit should NOT be instance of Exception'\n\n# === Tuple containing BaseException ===\ncaught_by_tuple_with_base = False\ntry:\n    raise KeyboardInterrupt()\nexcept (ValueError, BaseException):\n    caught_by_tuple_with_base = True\nassert caught_by_tuple_with_base, 'tuple with BaseException should catch KeyboardInterrupt'\n"
  },
  {
    "path": "crates/monty/test_cases/try_except__bare_raise_no_context.py",
    "content": "raise\n# Raise=RuntimeError('No active exception to reraise')\n"
  },
  {
    "path": "crates/monty/test_cases/try_except__invalid_type.py",
    "content": "try:\n    raise ValueError('test')\nexcept 123:\n    pass\n# Raise=TypeError('catching classes that do not inherit from BaseException is not allowed')\n"
  },
  {
    "path": "crates/monty/test_cases/tuple__getitem_out_of_bounds.py",
    "content": "a = (1, 2)\na[5]\n# Raise=IndexError('tuple index out of range')\n"
  },
  {
    "path": "crates/monty/test_cases/tuple__index_not_found.py",
    "content": "(1, 2, 3).index(4)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"tuple__index_not_found.py\", line 1, in <module>\n    (1, 2, 3).index(4)\n    ~~~~~~~~~~~~~~~~~~\nValueError: tuple.index(x): x not in tuple\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/tuple__index_start_gt_end.py",
    "content": "# Test that tuple.index with start > end doesn't panic but raises ValueError\n(1, 2, 3).index(1, 5, 2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"tuple__index_start_gt_end.py\", line 2, in <module>\n    (1, 2, 3).index(1, 5, 2)\n    ~~~~~~~~~~~~~~~~~~~~~~~~\nValueError: tuple.index(x): x not in tuple\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/tuple__methods.py",
    "content": "# === tuple.index() ===\nt = (1, 2, 3, 2)\nassert t.index(2) == 1, 'index finds first occurrence'\nassert t.index(3) == 2, 'index finds element'\nassert t.index(2, 2) == 3, 'index with start'\nassert t.index(2, 1, 4) == 1, 'index with start and end'\n\nt = ('a', 'b', 'c')\nassert t.index('b') == 1, 'index string in tuple'\n\n# === tuple.count() ===\nt = (1, 2, 2, 3, 2)\nassert t.count(2) == 3, 'count multiple occurrences'\nassert t.count(1) == 1, 'count single occurrence'\nassert t.count(4) == 0, 'count zero occurrences'\nassert ().count(1) == 0, 'count on empty tuple'\n\nt = ('a', 'b', 'a')\nassert t.count('a') == 2, 'count strings'\n"
  },
  {
    "path": "crates/monty/test_cases/tuple__ops.py",
    "content": "# === Empty tuple identity (singleton optimization) ===\n# In Python, () is () is always True because empty tuples are interned\nassert () is (), 'empty tuple identity'\nassert tuple() is (), 'tuple() is empty tuple'\nassert tuple() is tuple(), 'tuple() identity'\na = ()\nb = ()\nassert a is b, 'empty tuple vars are same object'\n# Empty tuple from operations\nassert (1,)[1:] is (), 'slice to empty is singleton'\nassert (1, 2) * 0 is (), 'mult by 0 is empty singleton'\n\n# === Tuple length ===\nassert len(()) == 0, 'len empty'\nassert len((1,)) == 1, 'len single'\nassert len((1, 2, 3)) == 3, 'len basic'\n\n# === Tuple indexing ===\na = (1, 2, 3)\nassert a[1] == 2, 'getitem basic'\n\na = ('a', 'b', 'c')\nassert a[0 - 2] == 'b', 'getitem negative'\nassert a[-1] == 'c', 'getitem -1'\n\n# === Nested tuples ===\nassert ((1, 2), (3, 4)) == ((1, 2), (3, 4)), 'nested tuple'\n\n# === Tuple repr/str ===\nassert repr((1, 2)) == '(1, 2)', 'tuple repr'\nassert str((1, 2)) == '(1, 2)', 'tuple str'\n\n# === Tuple concatenation (+) ===\nassert (1, 2) + (3, 4) == (1, 2, 3, 4), 'tuple add basic'\nassert () + (1, 2) == (1, 2), 'empty add tuple'\nassert (1, 2) + () == (1, 2), 'tuple add empty'\nassert () + () == (), 'empty add empty'\nassert ('a', 'b') + ('c',) == ('a', 'b', 'c'), 'tuple add strings'\nassert ((1, 2),) + ((3, 4),) == ((1, 2), (3, 4)), 'tuple add nested'\n\n# === Tuple repetition (*) ===\nassert (1, 2) * 3 == (1, 2, 1, 2, 1, 2), 'tuple mult int'\nassert 3 * (1, 2) == (1, 2, 1, 2, 1, 2), 'int mult tuple'\nassert (1,) * 0 == (), 'tuple mult zero'\nassert (1,) * -1 == (), 'tuple mult negative'\nassert () * 5 == (), 'empty tuple mult'\nassert (1, 2) * 1 == (1, 2), 'tuple mult one'\n\n# === Tuple augmented assignment edge cases ===\nt = ([1],)\ntry:\n    t[0] += [2]\n    assert False, 'tuple item augmented assignment should fail'\nexcept TypeError as e:\n    assert e.args == (\"'tuple' object does not support item assignment\",), 'tuple += error matches CPython'\n    assert t == ([1, 2],), 'inner list mutation happens before tuple store fails'\n\n# === tuple() constructor ===\nassert tuple() == (), 'tuple() empty'\nassert tuple([1, 2, 3]) == (1, 2, 3), 'tuple from list'\nassert tuple((1, 2, 3)) == (1, 2, 3), 'tuple from tuple'\nassert tuple(range(3)) == (0, 1, 2), 'tuple from range'\nassert tuple('abc') == ('a', 'b', 'c'), 'tuple from string'\nassert tuple(b'abc') == (97, 98, 99), 'tuple from bytes'\nassert tuple({'a': 1, 'b': 2}) == ('a', 'b'), 'tuple from dict yields keys'\n\n# non-ASCII strings (multi-byte UTF-8)\nassert tuple('héllo') == ('h', 'é', 'l', 'l', 'o'), 'tuple from string with accented char'\nassert tuple('日本') == ('日', '本'), 'tuple from string with CJK chars'\nassert tuple('a🎉b') == ('a', '🎉', 'b'), 'tuple from string with emoji'\n\n# === Tuple unpacking (PEP 448) ===\na = (1, 2)\nb = (3, 4)\nassert (*a,) == (1, 2), 'single tuple unpack'\nassert (*a, *b) == (1, 2, 3, 4), 'double tuple unpack'\nassert (0, *a, 5) == (0, 1, 2, 5), 'mixed tuple unpack'\nassert (*(),) == (), 'unpack empty tuple'\nassert (*[1, 2],) == (1, 2), 'unpack list into tuple'\n\n# === Tuple comparison (<, >, <=, >=) ===\nassert (1, 2) < (1, 3), 'lt second element differs'\nassert (1,) < (2,), 'lt single element'\nassert () < (1,), 'lt empty vs non-empty'\nassert (1, 2) < (1, 2, 3), 'lt shorter tuple'\nassert not (1, 2) < (1, 2), 'not lt when equal'\nassert not (1, 3) < (1, 2), 'not lt when greater'\n\nassert (1, 3) > (1, 2), 'gt second element'\nassert (2,) > (1,), 'gt single element'\nassert (1,) > (), 'gt non-empty vs empty'\nassert (1, 2, 3) > (1, 2), 'gt longer tuple'\nassert not (1, 2) > (1, 2), 'not gt when equal'\n\nassert (1, 2) <= (1, 2), 'le when equal'\nassert (1, 2) <= (1, 3), 'le when less'\nassert not (1, 3) <= (1, 2), 'not le when greater'\n\nassert (1, 2) >= (1, 2), 'ge when equal'\nassert (1, 3) >= (1, 2), 'ge when greater'\nassert not (1, 2) >= (1, 3), 'not ge when less'\n\n# === Tuple comparison with sorted() ===\nassert sorted([(2, 'b'), (1, 'a')]) == [(1, 'a'), (2, 'b')], 'sorted tuples'\nassert sorted([(1, 'b'), (1, 'a')]) == [(1, 'a'), (1, 'b')], 'sorted tuples second element'\nassert sorted([(3,), (1,), (2,)]) == [(1,), (2,), (3,)], 'sorted single-element tuples'\n\n# === Nested tuple comparison ===\nassert ((1, 2), 3) < ((1, 3), 2), 'nested tuple comparison'\nassert (1, (2, 3)) < (1, (2, 4)), 'nested tuple inner comparison'\n\n# === Equal-but-unorderable elements (None, lists, dicts) ===\n# CPython checks __eq__ first; equal elements skip ordering comparison\nassert not (1, None) < (1, None), 'equal None elements not lt'\nassert (1, None) <= (1, None), 'equal None elements le'\nassert (1, None) >= (1, None), 'equal None elements ge'\nassert not (1, None) > (1, None), 'equal None elements not gt'\nassert (1, None) < (2, None), 'first element resolves before None'\nassert (1, [1, 2]) <= (1, [1, 2]), 'equal list elements le'\n\n# === Mixed types in tuple comparison ===\nassert (1,) < (2.0,), 'int vs float in tuple'\nassert (1.0,) < (2,), 'float vs int in tuple'\nassert (True,) < (2,), 'bool vs int in tuple'\nassert (False,) < (True,), 'False vs True in tuple'\nassert (1, 'a') < (1, 'b'), 'string comparison in tuple'\nassert ('a', 1) < ('b', 1), 'string first element in tuple'\n\n# === Empty and equal tuples ===\nassert not () < (), 'empty tuples not lt'\nassert () <= (), 'empty tuples le'\nassert () >= (), 'empty tuples ge'\nassert not () > (), 'empty tuples not gt'\n"
  },
  {
    "path": "crates/monty/test_cases/tuple__unpack_type_error.py",
    "content": "(*42,)\n# Raise=TypeError('Value after * must be an iterable, not int')\n"
  },
  {
    "path": "crates/monty/test_cases/type__builtin_attr_error.py",
    "content": "x = len\nx.nonexistent\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__builtin_attr_error.py\", line 2, in <module>\n    x.nonexistent\nAttributeError: 'builtin_function_or_method' object has no attribute 'nonexistent'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__bytes_negative.py",
    "content": "bytes(-1)\n# Raise=ValueError('negative count')\n"
  },
  {
    "path": "crates/monty/test_cases/type__cell_not_builtin.py",
    "content": "print(cell)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__cell_not_builtin.py\", line 1, in <module>\n    print(cell)\n          ~~~~\nNameError: name 'cell' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__exception_attr_error.py",
    "content": "# Regression test for: \"fmt() called on disabled variant\" panic\n# Type::Exception must be displayable in error messages.\ne = ValueError('test')\ne.nonexistent\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__exception_attr_error.py\", line 4, in <module>\n    e.nonexistent\nAttributeError: 'ValueError' object has no attribute 'nonexistent'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__float_conversion_error.py",
    "content": "float([1, 2])\n# Raise=TypeError(\"float() argument must be a string or a real number, not 'list'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type__float_repr_both_quotes.py",
    "content": "float(\"it's \\\"nice\\\"\")\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__float_repr_both_quotes.py\", line 1, in <module>\n    float(\"it's \\\"nice\\\"\")\n    ~~~~~~~~~~~~~~~~~~~~~~\nValueError: could not convert string to float: 'it\\'s \"nice\"'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__float_repr_newline.py",
    "content": "float(\"a\\nb\")\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__float_repr_newline.py\", line 1, in <module>\n    float(\"a\\nb\")\n    ~~~~~~~~~~~~~\nValueError: could not convert string to float: 'a\\nb'\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__float_repr_single_quote.py",
    "content": "float(\"it's\")\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__float_repr_single_quote.py\", line 1, in <module>\n    float(\"it's\")\n    ~~~~~~~~~~~~~\nValueError: could not convert string to float: \"it's\"\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__int_conversion_error.py",
    "content": "int([1, 2])\n# Raise=TypeError(\"int() argument must be a string, a bytes-like object or a real number, not 'list'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type__list_not_iterable.py",
    "content": "list(123)\n# Raise=TypeError(\"'int' object is not iterable\")\n"
  },
  {
    "path": "crates/monty/test_cases/type__non_builtin_name_error.py",
    "content": "print(TextIOWrapper)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"type__non_builtin_name_error.py\", line 1, in <module>\n    print(TextIOWrapper)\n          ~~~~~~~~~~~~~\nNameError: name 'TextIOWrapper' is not defined\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/type__ops.py",
    "content": "# === type() function ===\nassert type(1) == int, 'type(int) returns int'\nassert type(1.5) == float, 'type(float) returns float'\nassert type(True) == bool, 'type(bool) returns bool'\nassert type('hello') == str, 'type(str) returns str'\nassert type([1, 2]) == list, 'type(list) returns list'\nassert type((1, 2)) == tuple, 'type(tuple) returns tuple'\nassert type({1: 2}) == dict, 'type(dict) returns dict'\nassert type(b'hi') == bytes, 'type(bytes) returns bytes'\nassert type(None) == type(None), 'type(None) is consistent'\n\n# === type() inequality ===\nassert type(1) != str, 'int type != str'\nassert type([]) != tuple, 'list type != tuple'\nassert type({}) != list, 'dict type != list'\nassert type(1) != float, 'int type != float'\n\n# === type repr ===\nassert repr(int) == \"<class 'int'>\", 'int type repr'\nassert repr(float) == \"<class 'float'>\", 'float type repr'\nassert repr(bool) == \"<class 'bool'>\", 'bool type repr'\nassert repr(str) == \"<class 'str'>\", 'str type repr'\nassert repr(list) == \"<class 'list'>\", 'list type repr'\nassert repr(tuple) == \"<class 'tuple'>\", 'tuple type repr'\nassert repr(dict) == \"<class 'dict'>\", 'dict type repr'\nassert repr(bytes) == \"<class 'bytes'>\", 'bytes type repr'\n\n# === type identity ===\nassert int is int, 'int is int'\nassert str is str, 'str is str'\nassert list is list, 'list is list'\nassert type(1) is int, 'type(1) is int'\nassert type('') is str, 'type str is str'\nassert type([]) is list, 'type([]) is list'\n\n# === list() constructor ===\nassert list() == [], 'list() empty'\nassert list([1, 2, 3]) == [1, 2, 3], 'list(list) copy'\nassert list((1, 2, 3)) == [1, 2, 3], 'list(tuple) convert'\nassert list(range(3)) == [0, 1, 2], 'list(range) convert'\nassert list('abc') == ['a', 'b', 'c'], 'list(str) split chars'\nassert list('') == [], 'list empty str'\n\n# list copy is independent\norig = [1, 2, 3]\ncopy = list(orig)\ncopy.append(4)\nassert orig == [1, 2, 3], 'list copy is independent'\nassert copy == [1, 2, 3, 4], 'list copy modified'\n\n# === tuple() constructor ===\nassert tuple() == (), 'tuple() empty'\nassert tuple([1, 2, 3]) == (1, 2, 3), 'tuple(list) convert'\nassert tuple((1, 2)) == (1, 2), 'tuple(tuple) copy'\nassert tuple(range(3)) == (0, 1, 2), 'tuple(range) convert'\nassert tuple('ab') == ('a', 'b'), 'tuple(str) split chars'\nassert tuple('') == (), 'tuple empty str'\n\n# === dict() constructor ===\nassert dict() == {}, 'dict() empty'\nassert dict({1: 2}) == {1: 2}, 'dict(dict) copy'\nassert dict({'a': 1, 'b': 2}) == {'a': 1, 'b': 2}, 'dict(dict) multiple keys'\n\n# dict copy is independent\norig_dict = {1: 2}\ncopy_dict = dict(orig_dict)\ncopy_dict[3] = 4\nassert orig_dict == {1: 2}, 'dict copy is independent'\nassert copy_dict == {1: 2, 3: 4}, 'dict copy modified'\nassert dict([('a', 1), ('b', 2)]) == {'a': 1, 'b': 2}, 'dict(list of tuples)'\nassert dict((('a', 1), ('b', 2))) == {'a': 1, 'b': 2}, 'dict(tuple of tuples)'\n\nheaders = ['a', 'b']\nrow_data = [1, 2]\nassert dict(zip(headers, row_data)) == {'a': 1, 'b': 2}, 'dict(zip(list, list))'\nassert dict(zip(['a', 'b'], [1])) == {'a': 1}, 'dict(zip()) truncates to shortest iterable'\n\nassert dict(a=1, b=2) == {'a': 1, 'b': 2}, 'dict keyword arguments'\nassert dict([('a', 1)], b=2) == {'a': 1, 'b': 2}, 'dict positional iterable plus kwargs'\nassert dict([('a', 1)], a=2) == {'a': 2}, 'dict kwargs overwrite positional iterable values'\n\n# === str() constructor ===\nassert str() == '', 'str() empty'\nassert str(123) == '123', 'str(int)'\nassert str(-42) == '-42', 'str(negative int)'\nassert str(0) == '0', 'str(zero)'\nassert str(1.5) == '1.5', 'str(float)'\nassert str(True) == 'True', 'str(bool True)'\nassert str(False) == 'False', 'str(bool False)'\nassert str(None) == 'None', 'str(None)'\nassert str([1, 2]) == '[1, 2]', 'str(list)'\nassert str((1, 2)) == '(1, 2)', 'str(tuple)'\nassert str({1: 2}) == '{1: 2}', 'str(dict)'\nassert str('hello') == 'hello', 'str(str)'\nassert str(b'hi') == \"b'hi'\", 'str(bytes)'\n\n# === bytes() constructor ===\nassert bytes() == b'', 'bytes() empty'\nassert bytes(3) == b'\\x00\\x00\\x00', 'bytes(int) zero-filled'\nassert bytes(0) == b'', 'bytes(0) empty'\nassert bytes(b'hi') == b'hi', 'bytes(bytes) copy'\n\n# === int() constructor ===\nassert int() == 0, 'int() default'\nassert int(42) == 42, 'int(int)'\nassert int(-5) == -5, 'int(negative int)'\nassert int(3.7) == 3, 'int(float) truncates down'\nassert int(-3.7) == -3, 'int(negative float) truncates toward zero'\nassert int(3.0) == 3, 'int(whole float)'\nassert int(True) == 1, 'int(True)'\nassert int(False) == 0, 'int(False)'\n\n# int() with extreme float values (should clamp to i64 range in Monty)\n# Note: Python uses arbitrary precision; Monty clamps to i64\nassert isinstance(int(1e18), int), 'int(large float) returns int'\nassert isinstance(int(-1e18), int), 'int(large negative float) returns int'\nassert int(0.0) == 0, 'int(0.0) is zero'\nassert int(-0.0) == 0, 'int(-0.0) is zero'\nassert int(0.9) == 0, 'int(0.9) truncates to 0'\nassert int(-0.9) == 0, 'int(-0.9) truncates to 0'\n\n# === float() constructor ===\nassert float() == 0.0, 'float() default'\nassert float(42) == 42.0, 'float(int)'\nassert float(-5) == -5.0, 'float(negative int)'\nassert float(3.14) == 3.14, 'float(float)'\nassert float(True) == 1.0, 'float(True)'\nassert float(False) == 0.0, 'float(False)'\n\n# === bool() constructor ===\nassert bool() == False, 'bool() default'\nassert bool(0) == False, 'bool(0)'\nassert bool(1) == True, 'bool(1)'\nassert bool(-1) == True, 'bool(-1)'\nassert bool(0.0) == False, 'bool(0.0)'\nassert bool(1.5) == True, 'bool(1.5)'\nassert bool('') == False, 'bool empty str'\nassert bool('x') == True, 'bool non-empty str'\nassert bool([]) == False, 'bool empty list'\nassert bool([1]) == True, 'bool non-empty list'\nassert bool(()) == False, 'bool empty tuple'\nassert bool((1,)) == True, 'bool non-empty tuple'\nassert bool({}) == False, 'bool empty dict'\nassert bool({1: 2}) == True, 'bool non-empty dict'\nassert bool(None) == False, 'bool(None)'\n\n# === isinstance() ===\nassert isinstance(1, int), 'isinstance int'\nassert isinstance(1.5, float), 'isinstance float'\nassert isinstance(True, bool), 'isinstance bool'\nassert isinstance('hello', str), 'isinstance str'\nassert isinstance([1, 2], list), 'isinstance list'\nassert isinstance((1, 2), tuple), 'isinstance tuple'\nassert isinstance({1: 2}, dict), 'isinstance dict'\nassert isinstance(b'hi', bytes), 'isinstance bytes'\n\n# isinstance negative cases\nassert not isinstance(1, str), 'isinstance int not str'\nassert not isinstance('x', int), 'isinstance str not int'\nassert not isinstance([], dict), 'isinstance list not dict'\n\n# isinstance with tuple of types\nassert isinstance(1, (int, str)), 'isinstance tuple match first'\nassert isinstance('x', (int, str)), 'isinstance tuple match second'\nassert not isinstance([], (int, str)), 'isinstance tuple no match'\nassert isinstance(1, (str, float, int)), 'isinstance tuple match third'\n\n# bool is subtype of int\nassert isinstance(True, int), 'bool is instance of int'\nassert isinstance(False, int), 'False is instance of int'\nassert isinstance(True, (int, str)), 'bool matches int in tuple'\n\n# isinstance with exception types\nerr = ValueError('test')\nassert isinstance(err, ValueError), 'isinstance exception'\nassert isinstance(err, Exception), 'isinstance exception base type'\nassert not isinstance(err, TypeError), 'isinstance exception wrong type'\nassert isinstance(err, (ValueError, TypeError)), 'isinstance exception tuple'\n\n# isinstance with nested tuples\nassert isinstance('a', (int, (str, bytes))), 'isinstance nested tuple match'\nassert isinstance(1, ((str, float), int)), 'isinstance deeply nested'\nassert not isinstance([], (int, (str, bytes))), 'isinstance nested tuple no match'\n\n# NoneType capitalization\nassert repr(type(None)) == \"<class 'NoneType'>\", 'NoneType capitalized'\n\n# === type().__name__ ===\nassert type(42).__name__ == 'int', 'int type name'\nassert type('hello').__name__ == 'str', 'str type name'\nassert type(True).__name__ == 'bool', 'bool type name'\nassert type(None).__name__ == 'NoneType', 'NoneType name'\nassert type([1, 2]).__name__ == 'list', 'list type name'\nassert type({'a': 1}).__name__ == 'dict', 'dict type name'\n\n# type().__name__ for exceptions\ntry:\n    raise ValueError('test')\nexcept ValueError as e:\n    assert type(e).__name__ == 'ValueError', 'exception type name'\n"
  },
  {
    "path": "crates/monty/test_cases/type__shadow_exc.py",
    "content": "# Builtin exception type 'ValueError' can be shadowed by assignment\nValueError = 'not an exception'\nassert ValueError == 'not an exception', 'ValueError shadowed'\n"
  },
  {
    "path": "crates/monty/test_cases/type__shadow_int.py",
    "content": "# Builtin type name 'int' can be shadowed by assignment\nint = 42\nassert int == 42, 'int shadowed by assignment'\n\n# for loop variable shadows builtin\nresult = []\nfor int in range(3):\n    result.append(int)\nassert result == [0, 1, 2], 'int works as for loop variable'\n"
  },
  {
    "path": "crates/monty/test_cases/type__shadow_len.py",
    "content": "# Builtin function 'len' can be shadowed by assignment\nlen = 'shadowed'\nassert len == 'shadowed', 'len shadowed by assignment'\n"
  },
  {
    "path": "crates/monty/test_cases/type__tuple_not_iterable.py",
    "content": "tuple(123)\n# Raise=TypeError(\"'int' object is not iterable\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_add_list.py",
    "content": "2 + [1]\n# Raise=TypeError(\"unsupported operand type(s) for +: 'int' and 'list'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_div_str.py",
    "content": "5 / 'x'\n# Raise=TypeError(\"unsupported operand type(s) for /: 'int' and 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_floordiv_str.py",
    "content": "5 // 'x'\n# Raise=TypeError(\"unsupported operand type(s) for //: 'int' and 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_iadd_str.py",
    "content": "x = 5\nx += 'a'\n# Raise=TypeError(\"unsupported operand type(s) for +=: 'int' and 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_mod_str.py",
    "content": "5 % 'x'\n# Raise=TypeError(\"unsupported operand type(s) for %: 'int' and 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_pow_str.py",
    "content": "5 ** 'x'\n# Raise=TypeError(\"unsupported operand type(s) for ** or pow(): 'int' and 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__int_sub_str.py",
    "content": "5 - 'x'\n# Raise=TypeError(\"unsupported operand type(s) for -: 'int' and 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__list_add_int.py",
    "content": "[1, 2] + 3\n# Raise=TypeError('can only concatenate list (not \"int\") to list')\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__list_add_str.py",
    "content": "[1] + 'x'\n# Raise=TypeError('can only concatenate list (not \"str\") to list')\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__list_iadd_int.py",
    "content": "# xfail=monty\n# Monty's list += only supports other lists, not arbitrary iterables.\n# CPython's list += calls extend() which requires an iterable.\nx = [1]\nx += 2\n# Raise=TypeError(\"'int' object is not iterable\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__str_add_int.py",
    "content": "'hello' + 1\n# Raise=TypeError('can only concatenate str (not \"int\") to str')\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__str_iadd_int.py",
    "content": "x = 'hello'\nx += 1\n# Raise=TypeError('can only concatenate str (not \"int\") to str')\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__unary_invert_str.py",
    "content": "# bitwise NOT on string should raise TypeError\n~'hello'\n# Raise=TypeError(\"bad operand type for unary ~: 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__unary_minus_str.py",
    "content": "# unary minus on heap-allocated string should raise TypeError\n# str() creates a heap-allocated string, triggering ref count check\n-str(42)\n# Raise=TypeError(\"bad operand type for unary -: 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__unary_neg_str.py",
    "content": "# unary minus on string should raise TypeError\n-'hello'\n# Raise=TypeError(\"bad operand type for unary -: 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/type_error__unary_plus_str.py",
    "content": "# unary plus on heap-allocated string should raise TypeError\n# str() creates a heap-allocated string, triggering ref count check\n+str(42)\n# Raise=TypeError(\"bad operand type for unary +: 'str'\")\n"
  },
  {
    "path": "crates/monty/test_cases/typing__types.py",
    "content": "# Tests for typing module type() behavior\n#\n# CPython's typing module uses various internal types for different constructs.\n# Monty simplifies this by using typing._SpecialForm for all typing markers.\n# Where CPython also uses _SpecialForm, we use == for exact match.\n# Where CPython uses different internal types, we accept both representations.\n\nimport typing\n\n# === Types that match between CPython and Monty ===\nassert repr(type(typing.Optional)) == \"<class 'typing._SpecialForm'>\", 'type(Optional)'\nassert repr(type(typing.ClassVar)) == \"<class 'typing._SpecialForm'>\", 'type(ClassVar)'\nassert repr(type(typing.Final)) == \"<class 'typing._SpecialForm'>\", 'type(Final)'\nassert repr(type(typing.Union)) == \"<class 'type'>\", 'type(Union)'\n\n# === Types that differ between CPython and Monty ===\n# CPython uses specialized internal types; Monty uses _SpecialForm for all\nassert repr(type(typing.Any)) in (\"<class 'typing._SpecialForm'>\", \"<class 'typing._AnyMeta'>\"), 'type(Any)'\nassert repr(type(typing.Callable)) in (\"<class 'typing._SpecialForm'>\", \"<class 'typing._CallableType'>\"), (\n    'type(Callable)'\n)\n\n# === Verify TYPE_CHECKING is False ===\nassert typing.TYPE_CHECKING is False, 'TYPE_CHECKING should be False at runtime'\n"
  },
  {
    "path": "crates/monty/test_cases/unpack__nested.py",
    "content": "# Test nested tuple unpacking\n\n# === Basic nested unpacking ===\ndata = ((1, 2), 'x')\n(a, b), c = data\nassert a == 1, 'nested unpack first inner'\nassert b == 2, 'nested unpack second inner'\nassert c == 'x', 'nested unpack outer'\n\n# === Deeply nested ===\n((a, b), (c, d)) = ((1, 2), (3, 4))\nassert a == 1, 'deep nested first'\nassert b == 2, 'deep nested second'\nassert c == 3, 'deep nested third'\nassert d == 4, 'deep nested fourth'\n\n# === Mixed depths ===\n(a, (b, c)) = (1, (2, 3))\nassert a == 1, 'mixed depth outer'\nassert b == 2, 'mixed depth inner first'\nassert c == 3, 'mixed depth inner second'\n\n# === Three levels deep ===\n(a, (b, (c, d))) = (1, (2, (3, 4)))\nassert a == 1, 'three level outer'\nassert b == 2, 'three level mid'\nassert c == 3, 'three level inner first'\nassert d == 4, 'three level inner second'\n\n# === In for loops ===\nitems = [((1, 2), 'a'), ((3, 4), 'b')]\nsums = []\nletters = []\nfor (a, b), c in items:\n    sums.append(a + b)\n    letters.append(c)\nassert sums == [3, 7], 'for loop nested unpack sums'\nassert letters == ['a', 'b'], 'for loop nested unpack letters'\n\n# === In comprehensions ===\nitems = [((1, 2), 'a'), ((3, 4), 'b')]\nresult = [a + b for (a, b), c in items]\nassert result == [3, 7], 'comprehension nested unpack'\n\n# === Deep nested in comprehension ===\nitems = [((1, 2), (3, 4)), ((5, 6), (7, 8))]\nresult = [a + b + c + d for (a, b), (c, d) in items]\nassert result == [10, 26], 'comprehension deep nested unpack'\n"
  },
  {
    "path": "crates/monty/test_cases/unpack__non_sequence.py",
    "content": "a, b = 42\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"unpack__non_sequence.py\", line 1, in <module>\n    a, b = 42\n    ~~~~\nTypeError: cannot unpack non-iterable int object\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/unpack__not_enough.py",
    "content": "a, b, c = (1, 2)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"unpack__not_enough.py\", line 1, in <module>\n    a, b, c = (1, 2)\n    ~~~~~~~\nValueError: not enough values to unpack (expected 3, got 2)\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/unpack__ops.py",
    "content": "# === Basic tuple unpacking ===\na, b = (1, 2)\nassert a == 1, 'first element of tuple'\nassert b == 2, 'second element of tuple'\n\n# === Unpacking without parentheses ===\nx, y = 10, 20\nassert x == 10, 'first element without parens'\nassert y == 20, 'second element without parens'\n\n# === Three element unpacking ===\na, b, c = (1, 2, 3)\nassert a == 1, 'three elements: first'\nassert b == 2, 'three elements: second'\nassert c == 3, 'three elements: third'\n\n\n# === Unpacking from function return ===\ndef returns_pair():\n    return 42, 37\n\n\nx, y = returns_pair()\nassert x == 42, 'function return first'\nassert y == 37, 'function return second'\n\n\ndef returns_triple():\n    return 'a', 'b', 'c'\n\n\np, q, r = returns_triple()\nassert p == 'a', 'function return triple first'\nassert q == 'b', 'function return triple second'\nassert r == 'c', 'function return triple third'\n\n# === Unpacking list ===\na, b = [100, 200]\nassert a == 100, 'list unpack first'\nassert b == 200, 'list unpack second'\n\na, b, c, d = [1, 2, 3, 4]\nassert a == 1, 'four element list first'\nassert d == 4, 'four element list fourth'\n\n# === Unpacking string ===\na, b = 'xy'\nassert a == 'x', 'string unpack first char'\nassert b == 'y', 'string unpack second char'\n\np, q, r = 'abc'\nassert p == 'a', 'three char string first'\nassert q == 'b', 'three char string second'\nassert r == 'c', 'three char string third'\n\n# === Unpacking with different value types ===\na, b = (True, False)\nassert a is True, 'bool tuple first'\nassert b is False, 'bool tuple second'\n\na, b = (1.5, 2.5)\nassert a == 1.5, 'float tuple first'\nassert b == 2.5, 'float tuple second'\n\na, b = (None, 42)\nassert a is None, 'mixed tuple None'\nassert b == 42, 'mixed tuple int'\n\n# === Unpacking with nested containers ===\na, b = ([1, 2], [3, 4])\nassert a == [1, 2], 'nested list first'\nassert b == [3, 4], 'nested list second'\n\na, b = ((1, 2), (3, 4))\nassert a == (1, 2), 'nested tuple first'\nassert b == (3, 4), 'nested tuple second'\n\n# === Reassignment via unpacking ===\nx = 1\ny = 2\nx, y = y, x\nassert x == 2, 'swap first'\nassert y == 1, 'swap second'\n\n# === Single element tuple (edge case) ===\n# Note: (x,) = (1,) is valid Python\n(a,) = (42,)\nassert a == 42, 'single element tuple unpack'\n\n(a,) = [99]\nassert a == 99, 'single element list unpack'\n\n(a,) = 'z'\nassert a == 'z', 'single char string unpack'\n\n# === Star unpacking (extended unpacking) ===\n# Star at end\nfirst, *rest = [1, 2, 3, 4, 5]\nassert first == 1, 'star at end: first'\nassert rest == [2, 3, 4, 5], 'star at end: rest'\n\n# Star at start\n*init, last = [1, 2, 3, 4, 5]\nassert init == [1, 2, 3, 4], 'star at start: init'\nassert last == 5, 'star at start: last'\n\n# Star in middle\nfirst, *middle, last = [1, 2, 3, 4, 5]\nassert first == 1, 'star in middle: first'\nassert middle == [2, 3, 4], 'star in middle: middle'\nassert last == 5, 'star in middle: last'\n\n# Empty rest (minimum values)\nfirst, *rest, last = [1, 2]\nassert first == 1, 'empty rest: first'\nassert rest == [], 'empty rest: rest is empty list'\nassert last == 2, 'empty rest: last'\n\n# From tuple\na, *b = (10, 20, 30)\nassert a == 10, 'star from tuple: a'\nassert b == [20, 30], 'star from tuple: b is list'\n\n# From string\nfirst, *mid, last = 'abcde'\nassert first == 'a', 'star from string: first'\nassert mid == ['b', 'c', 'd'], 'star from string: mid'\nassert last == 'e', 'star from string: last'\n\n# With more targets before star\na, b, c, *rest = [1, 2, 3, 4, 5, 6]\nassert a == 1, 'multiple before star: a'\nassert b == 2, 'multiple before star: b'\nassert c == 3, 'multiple before star: c'\nassert rest == [4, 5, 6], 'multiple before star: rest'\n\n# With more targets after star\n*init, x, y, z = [1, 2, 3, 4, 5, 6]\nassert init == [1, 2, 3], 'multiple after star: init'\nassert x == 4, 'multiple after star: x'\nassert y == 5, 'multiple after star: y'\nassert z == 6, 'multiple after star: z'\n\n# Star captures all but one\nhead, *tail = [1]\nassert head == 1, 'single item: head'\nassert tail == [], 'single item: tail is empty'\n\n# Star with bracket syntax\n[a, *b, c] = [1, 2, 3, 4]\nassert a == 1, 'bracket syntax: a'\nassert b == [2, 3], 'bracket syntax: b'\nassert c == 4, 'bracket syntax: c'\n"
  },
  {
    "path": "crates/monty/test_cases/unpack__star_not_enough.py",
    "content": "a, *b, c = [1]\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"unpack__star_not_enough.py\", line 1, in <module>\n    a, *b, c = [1]\n    ~~~~~~~~\nValueError: not enough values to unpack (expected at least 2, got 1)\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/unpack__too_many.py",
    "content": "a, b = (1, 2, 3, 4, 5)\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"unpack__too_many.py\", line 1, in <module>\n    a, b = (1, 2, 3, 4, 5)\n    ~~~~\nValueError: too many values to unpack (expected 2, got 5)\n\"\"\"\n"
  },
  {
    "path": "crates/monty/test_cases/version__cpython.py",
    "content": "import sys\n\nv = sys.version_info\nassert (v[0], v[1]) == (3, 14), f'Expected Python 3.14, got ({v[0]}, {v[1]})'\n"
  },
  {
    "path": "crates/monty/test_cases/walrus__all.py",
    "content": "# === Basic walrus operator ===\n# Simple assignment expression\nassert (x := 5) == 5, 'walrus returns value'\nassert x == 5, 'walrus assigns to variable'\n\n# Walrus in parentheses\ny = (z := 10)\nassert y == 10, 'walrus value can be assigned'\nassert z == 10, 'walrus target is assigned'\n\n# simple if\nx = None\nanswer = 'unset'\nif y := x:\n    answer = f'x is {y}'\n\nassert answer == 'unset'\n\nx = 123\nif y := x:\n    answer = f'x is {y}'\n\nassert answer == 'x is 123'\nx = 0\nif y := x:\n    answer = f'x is {y}'\nelse:\n    answer = 'x is unset'\n\nassert answer == 'x is unset'\n\n# === Walrus in if conditions ===\nif (a := 3) > 0:\n    assert a == 3, 'walrus in if test'\nelse:\n    assert False, 'should not reach else'\n\n# With falsy value\nif b := 0:\n    assert False, 'should not reach truthy branch'\nelse:\n    assert b == 0, 'walrus assigns even when falsy'\n\n# === Walrus in while loops ===\ncounter = 0\nresult = []\nwhile (n := counter) < 3:\n    result.append(n)\n    counter += 1\nassert result == [0, 1, 2], 'walrus in while condition'\nassert n == 3, 'walrus value persists after while'\n\n# === Nested walrus ===\n# Inner walrus assigned first, then outer\nassert (outer := (inner := 7) + 1) == 8, 'nested walrus returns correct value'\nassert inner == 7, 'inner walrus assigned'\nassert outer == 8, 'outer walrus assigned'\n\n# === Walrus in list literals ===\nitems = [(v := 1), v + 1, v + 2]\nassert items == [1, 2, 3], 'walrus in list literal'\nassert v == 1, 'walrus variable accessible after list'\n\n# === Walrus in ternary expressions ===\nresult = (t := 5) if True else 0\nassert result == 5, 'walrus in ternary truthy branch'\nassert t == 5, 'walrus assigned in ternary'\n\nresult2 = 0 if False else (f := 6)\nassert result2 == 6, 'walrus in ternary falsy branch'\nassert f == 6, 'walrus assigned in falsy branch'\n\n# === Walrus in dict/set literals ===\nd = {(k := 'key'): (val := 42)}\nassert d == {'key': 42}, 'walrus in dict literal'\nassert k == 'key', 'walrus key assigned'\nassert val == 42, 'walrus value assigned'\n\ns = {(s1 := 1), (s2 := 2)}\nassert s == {1, 2}, 'walrus in set literal'\nassert s1 == 1, 'walrus in set element 1'\nassert s2 == 2, 'walrus in set element 2'\n\n# === Walrus in subscript expressions ===\narr = [10, 20, 30]\nvalue = arr[(idx := 1)]\nassert value == 20, 'walrus in subscript index'\nassert idx == 1, 'walrus index assigned'\n\n\n# === Walrus in function calls ===\ndef identity(x):\n    return x\n\n\nresult = identity((arg := 99))\nassert result == 99, 'walrus in function argument'\nassert arg == 99, 'walrus arg assigned'\n\n# === Walrus with comparison operators ===\nassert (cmp := 10) > 5, 'walrus in comparison'\nassert cmp == 10, 'walrus assigned in comparison'\n\n# === Walrus in chained comparisons ===\n# Note: Chained comparisons like `0 < (mid := 5) < 10` are not yet supported\n# Testing a simpler comparison chain\nmid = (chain := 5)\nassert 0 < chain and chain < 10, 'walrus result used in comparison chain'\nassert mid == 5, 'walrus assigned correctly'\n\n# === Walrus in boolean expressions ===\n# Short-circuit with and\nresult = (first := 1) and (second := 2)\nassert result == 2, 'walrus in and expression'\nassert first == 1, 'first walrus assigned'\nassert second == 2, 'second walrus assigned (and evaluated)'\n\n# Short-circuit with or (second not evaluated)\nresult = (or_first := 1) or (or_skip := 999)\nassert result == 1, 'walrus in or expression (short-circuit)'\nassert or_first == 1, 'or first walrus assigned'\n\n# === Walrus with operations ===\nassert (op := 3) + 2 == 5, 'walrus with addition'\nassert op == 3, 'walrus assigned before operation'\n\n# === Walrus in f-strings ===\nmsg = f'{(fvar := \"hello\")} world'\nassert msg == 'hello world', 'walrus in f-string'\nassert fvar == 'hello', 'walrus assigned in f-string'\n\n# === Walrus with global ===\nglobal_var = None\n\n\ndef set_global():\n    global global_var\n    return (global_var := 'set')\n\n\nresult = set_global()\nassert result == 'set', 'walrus with global returns value'\nassert global_var == 'set', 'global var assigned via walrus'\n\n\n# === Walrus creates local in function scope ===\ndef func_scope():\n    if local := 42:\n        pass\n    return local\n\n\nassert func_scope() == 42, 'walrus creates local in function'\n\n# === Walrus in list comprehension element (leaks to enclosing scope) ===\n# Per PEP 572, walrus in comprehension assigns to enclosing scope\n# Note: walrus in comprehension iterable is not allowed, but in element/condition it is\nresult = [(leak := x) for x in range(3)]\nassert result == [0, 1, 2], 'walrus in comprehension element'\nassert leak == 2, 'walrus in comprehension leaks to enclosing scope'\n\n# === Walrus in comprehension condition ===\nresult = [x for x in range(5) if (limit := 3) and x < limit]\nassert result == [0, 1, 2], 'walrus in comprehension condition'\nassert limit == 3, 'walrus from comprehension condition accessible'\n\n# === Multiple walrus in same expression ===\nresult = (m1 := 1) + (m2 := 2) + (m3 := 3)\nassert result == 6, 'multiple walrus in expression'\nassert m1 == 1, 'first multi-walrus'\nassert m2 == 2, 'second multi-walrus'\nassert m3 == 3, 'third multi-walrus'\n\n# === Walrus in tuple ===\ntup = ((t1 := 'a'), (t2 := 'b'))\nassert tup == ('a', 'b'), 'walrus in tuple'\nassert t1 == 'a', 'first tuple walrus'\nassert t2 == 'b', 'second tuple walrus'\n"
  },
  {
    "path": "crates/monty/test_cases/while__all.py",
    "content": "# === Basic while loop ===\ni = 0\nresult = []\nwhile i < 3:\n    result.append(i)\n    i += 1\nassert result == [0, 1, 2], 'basic while loop'\n\n# === While with break ===\ni = 0\nresult = []\nwhile i < 10:\n    if i == 3:\n        break\n    result.append(i)\n    i += 1\nassert result == [0, 1, 2], 'while with break'\n\n# === While with continue ===\ni = 0\nresult = []\nwhile i < 5:\n    i += 1\n    if i % 2 == 0:\n        continue\n    result.append(i)\nassert result == [1, 3, 5], 'while with continue'\n\n# === While with else (no break - else runs) ===\ni = 0\nflag = 0\nwhile i < 3:\n    i += 1\nelse:\n    flag = 1\nassert flag == 1, 'while else runs when no break'\n\n# === While with else (with break - else skipped) ===\ni = 0\nflag = 0\nwhile i < 10:\n    i += 1\n    if i == 2:\n        break\nelse:\n    flag = 1\nassert flag == 0, 'while else skipped on break'\n\n# === while True with break ===\ni = 0\nresult = []\nwhile True:\n    result.append(i)\n    i += 1\n    if i >= 3:\n        break\nassert result == [0, 1, 2], 'while True with break'\n\n# === while False (never executes) ===\nflag = 0\nwhile False:\n    flag = 1\nassert flag == 0, 'while False never executes'\n\n# === while False with else (else runs immediately) ===\nflag = 0\nwhile False:\n    flag = 1\nelse:\n    flag = 2\nassert flag == 2, 'while False runs else immediately'\n\n# === Nested while loops ===\ni = 0\nresult = []\nwhile i < 2:\n    j = 0\n    while j < 2:\n        result.append((i, j))\n        j += 1\n    i += 1\nassert result == [(0, 0), (0, 1), (1, 0), (1, 1)], 'nested while loops'\n\n# === Nested while with break inner ===\ni = 0\nresult = []\nwhile i < 3:\n    j = 0\n    while j < 3:\n        if j == 1:\n            break\n        result.append((i, j))\n        j += 1\n    i += 1\nassert result == [(0, 0), (1, 0), (2, 0)], 'nested while break inner only'\n\n# === For inside while ===\ni = 0\nresult = []\nwhile i < 2:\n    for j in ['a', 'b']:\n        result.append((i, j))\n    i += 1\nassert result == [(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')], 'for inside while'\n\n# === While inside for ===\nresult = []\nfor i in [0, 1]:\n    j = 0\n    while j < 2:\n        result.append((i, j))\n        j += 1\nassert result == [(0, 0), (0, 1), (1, 0), (1, 1)], 'while inside for'\n\n# === Complex condition with and ===\ni = 0\nj = 10\nresult = []\nwhile i < 5 and j > 5:\n    result.append((i, j))\n    i += 1\n    j -= 1\nassert result == [(0, 10), (1, 9), (2, 8), (3, 7), (4, 6)], 'while with and condition'\n\n# === Complex condition with or ===\ni = 5\ncount = 0\nwhile i < 3 or count < 2:\n    count += 1\n    i += 1\nassert count == 2, 'while with or condition'\n\n\n# === While with function call condition ===\ndef check(n):\n    return n < 3\n\n\ni = 0\nresult = []\nwhile check(i):\n    result.append(i)\n    i += 1\nassert result == [0, 1, 2], 'while with function call condition'\n\n# === Continue does not skip else ===\ni = 0\nflag = 0\nwhile i < 3:\n    i += 1\n    if i == 2:\n        continue\nelse:\n    flag = 1\nassert flag == 1, 'continue does not skip else'\n\n# === Nested while - break outer via flag ===\ni = 0\nresult = []\ndone = False\nwhile i < 3 and not done:\n    j = 0\n    while j < 3:\n        if i == 1 and j == 1:\n            done = True\n            break\n        result.append((i, j))\n        j += 1\n    i += 1\nassert result == [(0, 0), (0, 1), (0, 2), (1, 0)], 'nested while with flag break'\n\n# === While with negative condition ===\ni = 5\nresult = []\nwhile not i == 3:\n    result.append(i)\n    i -= 1\nassert result == [5, 4], 'while with not condition'\n\n# === Nested while with inner else ===\ni = 0\nresult = []\nwhile i < 2:\n    j = 0\n    while j < 2:\n        result.append(j)\n        j += 1\n    else:\n        result.append('inner-else')\n    i += 1\nassert result == [0, 1, 'inner-else', 0, 1, 'inner-else'], 'nested while with inner else'\n\n# === Break in nested while skips inner else ===\ni = 0\nresult = []\nwhile i < 2:\n    j = 0\n    while j < 3:\n        if j == 1:\n            break\n        result.append(j)\n        j += 1\n    else:\n        result.append('inner-else')\n    i += 1\nassert result == [0, 0], 'break skips inner else only'\n"
  },
  {
    "path": "crates/monty/tests/asyncio.rs",
    "content": "//! Tests for async edge cases around ResolveFutures::resume behavior.\n//!\n//! These tests verify the behavior of the async execution model, specifically around\n//! resolving external futures incrementally via `ResolveFutures::resume()`.\n\nuse monty::{\n    ExcType, ExtFunctionResult, MontyException, MontyObject, MontyRun, NameLookupResult, NoLimitTracker, PrintWriter,\n    ResolveFutures, RunProgress,\n};\n\n/// Helper to create a MontyRun for async external function tests.\n///\n/// Sets up an async function that calls two async external functions (`foo` and `bar`)\n/// via asyncio.gather and returns their sum.\nfn create_gather_two_runner() -> MontyRun {\n    let code = r\"\nimport asyncio\n\nasync def main():\n    a, b = await asyncio.gather(foo(), bar())\n    return a + b\n\nawait main()\n\";\n    MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap()\n}\n\n/// Helper to create a MontyRun for async external function tests with three functions.\nfn create_gather_three_runner() -> MontyRun {\n    let code = r\"\nimport asyncio\n\nasync def main():\n    a, b, c = await asyncio.gather(foo(), bar(), baz())\n    return a + b + c\n\nawait main()\n\";\n    MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap()\n}\n\n/// Resolves consecutive `NameLookup` yields by providing a `Function` object for each name.\nfn resolve_name_lookups<T: monty::ResourceTracker>(\n    mut progress: RunProgress<T>,\n) -> Result<RunProgress<T>, monty::MontyException> {\n    while let RunProgress::NameLookup(lookup) = progress {\n        let name = lookup.name.clone();\n        progress = lookup.resume(\n            NameLookupResult::Value(MontyObject::Function { name, docstring: None }),\n            PrintWriter::Stdout,\n        )?;\n    }\n    Ok(progress)\n}\n\n/// Helper to drive execution through external calls until we get ResolveFutures.\n///\n/// Returns (pending_call_ids, state, collected_call_ids) where collected_call_ids\n/// are the call_ids from all the FunctionCalls we processed with resume_pending().\nfn drive_to_resolve_futures<T: monty::ResourceTracker>(mut progress: RunProgress<T>) -> (ResolveFutures<T>, Vec<u32>) {\n    let mut collected_call_ids = Vec::new();\n\n    loop {\n        match progress {\n            RunProgress::NameLookup(lookup) => {\n                let name = lookup.name.clone();\n                progress = lookup\n                    .resume(\n                        NameLookupResult::Value(MontyObject::Function { name, docstring: None }),\n                        PrintWriter::Stdout,\n                    )\n                    .unwrap();\n            }\n            RunProgress::FunctionCall(call) => {\n                collected_call_ids.push(call.call_id);\n                progress = call.resume_pending(PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::ResolveFutures(state) => {\n                return (state, collected_call_ids);\n            }\n            RunProgress::Complete(_) => {\n                panic!(\"unexpected Complete before ResolveFutures\");\n            }\n            RunProgress::OsCall(call) => {\n                panic!(\"unexpected OsCall: {:?}\", call.function);\n            }\n        }\n    }\n}\n\n// === Test: Resume with all call_ids at once ===\n\n#[test]\nfn resume_with_all_call_ids() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n    assert_eq!(call_ids.len(), 2, \"should have 2 pending calls\");\n\n    // Resume with all results at once\n    let results = vec![\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(10))),\n        (call_ids[1], ExtFunctionResult::Return(MontyObject::Int(32))),\n    ];\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n// === Test: Resume with partial results ===\n\n#[test]\nfn resume_with_partial_results() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Resume with only the first result\n    let results = vec![(call_ids[0], ExtFunctionResult::Return(MontyObject::Int(10)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // Should still need more futures resolved\n    let state = progress.into_resolve_futures().expect(\"should still need futures\");\n\n    // Resume with the second result\n    let results = vec![(call_ids[1], ExtFunctionResult::Return(MontyObject::Int(32)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n// === Test: Resume with unknown call_id ===\n\n#[test]\nfn resume_with_unknown_call_id() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, _call_ids) = drive_to_resolve_futures(progress);\n\n    // Resume with an unknown call_id\n    let results = vec![(9999, ExtFunctionResult::Return(MontyObject::Int(10)))];\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should error on unknown call_id\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::RuntimeError);\n    let msg = exc.message().unwrap();\n    assert!(\n        msg.contains(\"unknown call_id 9999\"),\n        \"error should mention unknown call_id, got: {msg}\"\n    );\n}\n\n// === Test: Resume with empty results ===\n\n#[test]\nfn resume_with_empty_results() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Resume with empty results - should still be blocked\n    let results: Vec<(u32, ExtFunctionResult)> = vec![];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // Should still need futures resolved\n    let state = progress.into_resolve_futures().expect(\"should still need futures\");\n\n    // Now resolve everything\n    let results = vec![\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(10))),\n        (call_ids[1], ExtFunctionResult::Return(MontyObject::Int(32))),\n    ];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n// === Test: Resume with error result ===\n\n#[test]\nfn resume_with_error_result() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Resume with one success and one error\n    let results = vec![\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(10))),\n        (\n            call_ids[1],\n            ExtFunctionResult::Error(MontyException::new(ExcType::ValueError, Some(\"test error\".to_string()))),\n        ),\n    ];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    // Should propagate the error\n    assert!(result.is_err(), \"should propagate error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::ValueError);\n    assert_eq!(exc.message(), Some(\"test error\"));\n}\n\n// === Test: Resume with three functions, reversed order ===\n\n#[test]\nfn resume_with_reversed_order() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Resume with results in reverse order - should still work\n    let results = vec![\n        (call_ids[1], ExtFunctionResult::Return(MontyObject::Int(32))), // bar() = 32\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(10))), // foo() = 10\n    ];\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n// === Test: Three-way gather with incremental resolution ===\n\n#[test]\nfn three_way_gather_incremental() {\n    let runner = create_gather_three_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n    assert_eq!(call_ids.len(), 3, \"should have 3 pending calls\");\n\n    // Resolve one at a time\n    let results = vec![(call_ids[0], ExtFunctionResult::Return(MontyObject::Int(100)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let state = progress.into_resolve_futures().expect(\"need more\");\n\n    let results = vec![(call_ids[1], ExtFunctionResult::Return(MontyObject::Int(200)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let state = progress.into_resolve_futures().expect(\"need more\");\n\n    let results = vec![(call_ids[2], ExtFunctionResult::Return(MontyObject::Int(300)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(600));\n}\n\n// === Test: Duplicate call_id in results (should be fine - second is ignored) ===\n\n#[test]\nfn resume_with_duplicate_call_id() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Include duplicate - second value should be ignored\n    let results = vec![\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(10))),\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(99))), // duplicate - ignored!\n        (call_ids[1], ExtFunctionResult::Return(MontyObject::Int(32))),\n    ];\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n// === Test: gather_error_propagated_as_exception ===\n\n#[test]\nfn gather_error_propagated_as_exception() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Both fail with errors\n    let results = vec![\n        (\n            call_ids[0],\n            ExtFunctionResult::Error(MontyException::new(ExcType::ValueError, Some(\"foo error\".to_string()))),\n        ),\n        (\n            call_ids[1],\n            ExtFunctionResult::Error(MontyException::new(\n                ExcType::RuntimeError,\n                Some(\"bar error\".to_string()),\n            )),\n        ),\n    ];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    // One of the errors should propagate (implementation may choose either)\n    assert!(result.is_err(), \"should propagate an error\");\n}\n\n// === Test: Sequential awaits - second fails ===\n\nfn create_sequential_awaits_runner() -> MontyRun {\n    let code = r\"\nimport asyncio\n\nasync def main():\n    a = await foo()\n    b = await bar()\n    return a + b\n\nawait main()\n\";\n    MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap()\n}\n\n#[test]\nfn sequential_awaits_second_fails() {\n    let runner = create_sequential_awaits_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n    let progress = resolve_name_lookups(progress).unwrap();\n\n    // First external call (foo)\n    let RunProgress::FunctionCall(call) = progress else {\n        panic!(\"expected FunctionCall for foo\");\n    };\n    let foo_call_id = call.call_id;\n    let progress = call.resume_pending(PrintWriter::Stdout).unwrap();\n\n    // Should yield for resolution\n    let state = progress.into_resolve_futures().expect(\"should need foo resolved\");\n    assert_eq!(state.pending_call_ids(), vec![foo_call_id]);\n\n    // Resolve foo successfully\n    let results = vec![(foo_call_id, ExtFunctionResult::Return(MontyObject::Int(10)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let progress = resolve_name_lookups(progress).unwrap();\n\n    // Second external call (bar)\n    let RunProgress::FunctionCall(call) = progress else {\n        panic!(\"expected FunctionCall for bar\");\n    };\n    let bar_call_id = call.call_id;\n    let progress = call.resume_pending(PrintWriter::Stdout).unwrap();\n\n    // Should yield for resolution\n    let state = progress.into_resolve_futures().expect(\"should need bar resolved\");\n    assert_eq!(state.pending_call_ids(), vec![bar_call_id]);\n\n    // Fail bar with an exception\n    let results = vec![(\n        bar_call_id,\n        ExtFunctionResult::Error(MontyException::new(ExcType::ValueError, Some(\"bar failed\".to_string()))),\n    )];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should propagate bar's error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::ValueError);\n    assert_eq!(exc.message(), Some(\"bar failed\"));\n}\n\n// === Test: Sequential awaits - first fails ===\n\n#[test]\nfn sequential_awaits_first_fails() {\n    let runner = create_sequential_awaits_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n    let progress = resolve_name_lookups(progress).unwrap();\n\n    // First external call (foo)\n    let RunProgress::FunctionCall(call) = progress else {\n        panic!(\"expected FunctionCall for foo\");\n    };\n    let foo_call_id = call.call_id;\n    let progress = call.resume_pending(PrintWriter::Stdout).unwrap();\n\n    let state = progress.into_resolve_futures().expect(\"should need foo resolved\");\n\n    // Fail foo with an exception - bar should never be called\n    let results = vec![(\n        foo_call_id,\n        ExtFunctionResult::Error(MontyException::new(\n            ExcType::RuntimeError,\n            Some(\"foo failed early\".to_string()),\n        )),\n    )];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should propagate foo's error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::RuntimeError);\n    assert_eq!(exc.message(), Some(\"foo failed early\"));\n}\n\n// === Test: Gather - first external fails before second is resolved ===\n\n#[test]\nfn gather_first_external_fails_immediately() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n    assert_eq!(call_ids.len(), 2, \"should have 2 calls\");\n\n    // Resolve first call with error, second with success\n    let results = vec![(\n        call_ids[0],\n        ExtFunctionResult::Error(MontyException::new(ExcType::ValueError, Some(\"foo failed\".to_string()))),\n    )];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    // Error should propagate immediately (no need to resolve second)\n    assert!(result.is_err(), \"should propagate foo's error immediately\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::ValueError);\n    assert_eq!(exc.message(), Some(\"foo failed\"));\n}\n\n// === Test: Gather - second external fails ===\n\n#[test]\nfn gather_second_external_fails() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // Resolve second call with error\n    let results = vec![(\n        call_ids[1],\n        ExtFunctionResult::Error(MontyException::new(\n            ExcType::RuntimeError,\n            Some(\"bar failed\".to_string()),\n        )),\n    )];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should propagate bar's error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::RuntimeError);\n    assert_eq!(exc.message(), Some(\"bar failed\"));\n}\n\n// === Test: Both gather tasks fail ===\n\n#[test]\nfn gather_both_fail() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    let results = vec![\n        (\n            call_ids[0],\n            ExtFunctionResult::Error(MontyException::new(ExcType::ValueError, Some(\"foo failed\".to_string()))),\n        ),\n        (\n            call_ids[1],\n            ExtFunctionResult::Error(MontyException::new(\n                ExcType::RuntimeError,\n                Some(\"bar failed\".to_string()),\n            )),\n        ),\n    ];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n    assert!(result.is_err(), \"should propagate one of the errors\");\n}\n\n// === Test: Three-way gather, partial error ===\n\n#[test]\nfn three_way_gather_partial_error() {\n    let runner = create_gather_three_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // First and third succeed, second fails\n    let results = vec![\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(100))),\n        (\n            call_ids[1],\n            ExtFunctionResult::Error(MontyException::new(\n                ExcType::TypeError,\n                Some(\"bar type error\".to_string()),\n            )),\n        ),\n        (call_ids[2], ExtFunctionResult::Return(MontyObject::Int(300))),\n    ];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n    assert!(result.is_err(), \"should propagate bar's error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::TypeError);\n}\n\n// === Test: Incremental resolution with error on second round ===\n\n#[test]\nfn incremental_resolution_error_on_second_round() {\n    let runner = create_gather_two_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    // First resolve one successfully\n    let results = vec![(call_ids[0], ExtFunctionResult::Return(MontyObject::Int(100)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let state = progress.into_resolve_futures().expect(\"need more\");\n\n    // Then fail the second\n    let results = vec![(\n        call_ids[1],\n        ExtFunctionResult::Error(MontyException::new(\n            ExcType::ValueError,\n            Some(\"delayed failure\".to_string()),\n        )),\n    )];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n    assert!(result.is_err(), \"should propagate delayed error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::ValueError);\n    assert_eq!(exc.message(), Some(\"delayed failure\"));\n}\n\n// === Test: Gather with all at once, mixed success/failure ===\n\n#[test]\nfn gather_three_all_at_once_mixed() {\n    let runner = create_gather_three_runner();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let (state, call_ids) = drive_to_resolve_futures(progress);\n\n    let results = vec![\n        (call_ids[0], ExtFunctionResult::Return(MontyObject::Int(100))),\n        (call_ids[1], ExtFunctionResult::Return(MontyObject::Int(200))),\n    ];\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n    let state = progress.into_resolve_futures().expect(\"need more\");\n\n    let results = vec![(\n        call_ids[2],\n        ExtFunctionResult::Error(MontyException::new(\n            ExcType::RuntimeError,\n            Some(\"baz failed\".to_string()),\n        )),\n    )];\n\n    let result = state.resume(results, PrintWriter::Stdout);\n    assert!(result.is_err(), \"should propagate baz error\");\n}\n\n// === Tests: Nested gather with task switching ===\n//\n// These tests target a pair of bugs in task switching during incremental resolution:\n// - Correct value pushing when restoring from a resolved task (Bug 1)\n// - Correct waiter context detection for current task (Bug 2)\n\n/// Helper to drive execution, collecting function calls and resolving them async,\n/// until we reach ResolveFutures. Returns the snapshot and a vec of\n/// (call_id, function_name) pairs for all external calls made.\nfn drive_collecting_calls<T: monty::ResourceTracker>(\n    mut progress: RunProgress<T>,\n) -> (ResolveFutures<T>, Vec<(u32, String)>) {\n    let mut collected = Vec::new();\n\n    loop {\n        match progress {\n            RunProgress::NameLookup(lookup) => {\n                let name = lookup.name.clone();\n                progress = lookup\n                    .resume(\n                        NameLookupResult::Value(MontyObject::Function { name, docstring: None }),\n                        PrintWriter::Stdout,\n                    )\n                    .unwrap();\n            }\n            RunProgress::FunctionCall(call) => {\n                collected.push((call.call_id, call.function_name.clone()));\n                progress = call.resume_pending(PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::ResolveFutures(state) => {\n                return (state, collected);\n            }\n            RunProgress::Complete(_) => {\n                panic!(\"unexpected Complete before ResolveFutures\");\n            }\n            RunProgress::OsCall(call) => {\n                panic!(\"unexpected OsCall: {:?}\", call.function);\n            }\n        }\n    }\n}\n\n/// Tests nested gathers where spawned tasks do sequential external await then inner gather.\n///\n/// Pattern:\n/// - Outer gather spawns 3 coroutine tasks\n/// - Each coroutine does `await get_lat_lng(city)` then `await asyncio.gather(get_temp(city), get_desc(city))`\n/// - All external functions are resolved via async futures\n///\n/// This exercises both Bug 1 (resolved value not pushed to restored task stack) and\n/// Bug 2 (current task's gather result pushed to wrong location).\n#[test]\nfn nested_gather_with_spawned_tasks_and_external_futures() {\n    let code = r\"\nimport asyncio\n\nasync def process(city):\n    coords = await get_lat_lng(city)\n    temp, desc = await asyncio.gather(get_temp(city), get_desc(city))\n    return coords + temp + desc\n\nasync def main():\n    results = await asyncio.gather(\n        process('a'),\n        process('b'),\n        process('c'),\n    )\n    return results[0] + results[1] + results[2]\n\nawait main()\n\";\n\n    let runner = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    // Drive until all initial external calls are made and we need to resolve futures\n    let (state, calls) = drive_collecting_calls(progress);\n\n    // The 3 spawned tasks each call get_lat_lng first, so we expect 3 get_lat_lng calls\n    assert_eq!(calls.len(), 3, \"should have 3 initial get_lat_lng calls\");\n    for (_, name) in &calls {\n        assert_eq!(name, \"get_lat_lng\", \"initial calls should all be get_lat_lng\");\n    }\n\n    // Resolve all 3 get_lat_lng calls: each returns 100\n    let results: Vec<(u32, ExtFunctionResult)> = calls\n        .iter()\n        .map(|(id, _)| (*id, ExtFunctionResult::Return(MontyObject::Int(100))))\n        .collect();\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // After resolving get_lat_lng, each task proceeds to the inner gather which\n    // calls get_temp and get_desc. Drive those calls.\n    let (state, calls) = drive_collecting_calls(progress);\n\n    // Each of 3 tasks calls get_temp + get_desc = 6 calls total\n    assert_eq!(calls.len(), 6, \"should have 6 inner gather calls (3 tasks * 2 each)\");\n    let temp_calls: Vec<_> = calls.iter().filter(|(_, n)| n == \"get_temp\").collect();\n    let desc_calls: Vec<_> = calls.iter().filter(|(_, n)| n == \"get_desc\").collect();\n    assert_eq!(temp_calls.len(), 3, \"should have 3 get_temp calls\");\n    assert_eq!(desc_calls.len(), 3, \"should have 3 get_desc calls\");\n\n    // Resolve all inner calls: get_temp returns 10, get_desc returns 1\n    let results: Vec<(u32, ExtFunctionResult)> = calls\n        .iter()\n        .map(|(id, name)| {\n            let val = if name == \"get_temp\" { 10 } else { 1 };\n            (*id, ExtFunctionResult::Return(MontyObject::Int(val)))\n        })\n        .collect();\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // Each task returns coords(100) + temp(10) + desc(1) = 111\n    // main returns 111 + 111 + 111 = 333\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(333));\n}\n\n/// Tests nested gathers with incremental resolution (one task at a time).\n///\n/// Same pattern as above but resolves futures in multiple rounds to ensure\n/// task switching between partially-resolved states works correctly.\n#[test]\nfn nested_gather_incremental_resolution() {\n    let code = r\"\nimport asyncio\n\nasync def process(x):\n    a = await step1(x)\n    b, c = await asyncio.gather(step2(x), step3(x))\n    return a + b + c\n\nasync def main():\n    r1, r2 = await asyncio.gather(process('x'), process('y'))\n    return r1 + r2\n\nawait main()\n\";\n\n    let runner = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    // Drive to get the initial step1 calls\n    let (state, calls) = drive_collecting_calls(progress);\n    assert_eq!(calls.len(), 2, \"should have 2 step1 calls\");\n\n    // Resolve only the FIRST step1 call\n    let results = vec![(calls[0].0, ExtFunctionResult::Return(MontyObject::Int(100)))];\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // First task proceeds to inner gather (step2 + step3), second task still blocked\n    let (state, new_calls) = drive_collecting_calls(progress);\n\n    // We should see step2 and step3 for the first task\n    assert_eq!(new_calls.len(), 2, \"should have 2 inner calls from first task\");\n\n    // Now resolve the second step1 call AND the first task's inner calls\n    let mut results: Vec<(u32, ExtFunctionResult)> = vec![\n        // Second task's step1\n        (calls[1].0, ExtFunctionResult::Return(MontyObject::Int(200))),\n    ];\n    // First task's inner calls\n    for (id, name) in &new_calls {\n        let val = if name == \"step2\" { 10 } else { 1 };\n        results.push((*id, ExtFunctionResult::Return(MontyObject::Int(val))));\n    }\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // Second task now proceeds to inner gather\n    let (state, final_calls) = drive_collecting_calls(progress);\n    assert_eq!(final_calls.len(), 2, \"should have 2 inner calls from second task\");\n\n    // Resolve second task's inner calls\n    let results: Vec<(u32, ExtFunctionResult)> = final_calls\n        .iter()\n        .map(|(id, name)| {\n            let val = if name == \"step2\" { 20 } else { 2 };\n            (*id, ExtFunctionResult::Return(MontyObject::Int(val)))\n        })\n        .collect();\n\n    let progress = state.resume(results, PrintWriter::Stdout).unwrap();\n\n    // First task: 100 + 10 + 1 = 111\n    // Second task: 200 + 20 + 2 = 222\n    // Total: 111 + 222 = 333\n    let result = progress.into_complete().expect(\"should complete\");\n    assert_eq!(result, MontyObject::Int(333));\n}\n"
  },
  {
    "path": "crates/monty/tests/binary_serde.rs",
    "content": "//! Tests for binary serialization and deserialization of `MontyRun` and `RunProgress`.\n//!\n//! These tests verify that execution state can be serialized with postcard for:\n//! - Caching parsed code to avoid re-parsing\n//! - Snapshotting execution state for external function calls\n\nuse monty::{MontyObject, MontyRun, NameLookupResult, NoLimitTracker, PrintWriter, RunProgress};\n\n/// Resolves consecutive `NameLookup` yields by providing a `Function` object for each name.\nfn resolve_name_lookups<T: monty::ResourceTracker>(\n    mut progress: RunProgress<T>,\n) -> Result<RunProgress<T>, monty::MontyException> {\n    while let RunProgress::NameLookup(lookup) = progress {\n        let name = lookup.name.clone();\n        progress = lookup.resume(\n            NameLookupResult::Value(MontyObject::Function { name, docstring: None }),\n            PrintWriter::Stdout,\n        )?;\n    }\n    Ok(progress)\n}\n\n// === MontyRun dump/load Tests ===\n\n#[test]\nfn monty_run_dump_load_simple() {\n    // Create a runner, dump it, load it, and verify it produces the same result\n    let runner = MontyRun::new(\"1 + 2\".to_owned(), \"test.py\", vec![]).unwrap();\n    let bytes = runner.dump().unwrap();\n    let loaded = MontyRun::load(&bytes).unwrap();\n\n    let result = loaded.run_no_limits(vec![]).unwrap();\n    assert_eq!(result, MontyObject::Int(3));\n}\n\n#[test]\nfn monty_run_dump_load_with_inputs() {\n    // Test that input names are preserved across dump/load\n    let runner = MontyRun::new(\"x + y * 2\".to_owned(), \"test.py\", vec![\"x\".to_owned(), \"y\".to_owned()]).unwrap();\n    let bytes = runner.dump().unwrap();\n    let loaded = MontyRun::load(&bytes).unwrap();\n\n    let result = loaded\n        .run_no_limits(vec![MontyObject::Int(10), MontyObject::Int(5)])\n        .unwrap();\n    assert_eq!(result, MontyObject::Int(20));\n}\n\n#[test]\nfn monty_run_dump_load_preserves_code() {\n    // Verify the code string is preserved\n    let code = \"def foo(x):\\n    return x * 2\\nfoo(21)\".to_owned();\n    let runner = MontyRun::new(code.clone(), \"test.py\", vec![]).unwrap();\n    let bytes = runner.dump().unwrap();\n    let loaded = MontyRun::load(&bytes).unwrap();\n\n    assert_eq!(loaded.code(), code);\n    let result = loaded.run_no_limits(vec![]).unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n#[test]\nfn monty_run_dump_load_complex_code() {\n    // Test with more complex code including functions, loops, conditionals\n    let code = r\"\ndef fib(n):\n    if n <= 1:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nresult = []\nfor i in range(10):\n    result.append(fib(i))\nresult\n\"\n    .to_owned();\n\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let bytes = runner.dump().unwrap();\n    let loaded = MontyRun::load(&bytes).unwrap();\n\n    let result = loaded.run_no_limits(vec![]).unwrap();\n    // First 10 Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34\n    let expected = MontyObject::List(vec![\n        MontyObject::Int(0),\n        MontyObject::Int(1),\n        MontyObject::Int(1),\n        MontyObject::Int(2),\n        MontyObject::Int(3),\n        MontyObject::Int(5),\n        MontyObject::Int(8),\n        MontyObject::Int(13),\n        MontyObject::Int(21),\n        MontyObject::Int(34),\n    ]);\n    assert_eq!(result, expected);\n}\n\n#[test]\nfn monty_run_dump_load_multiple_runs() {\n    // A loaded runner can be run multiple times\n    let runner = MontyRun::new(\"x * 2\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let bytes = runner.dump().unwrap();\n    let loaded = MontyRun::load(&bytes).unwrap();\n\n    assert_eq!(\n        loaded.run_no_limits(vec![MontyObject::Int(5)]).unwrap(),\n        MontyObject::Int(10)\n    );\n    assert_eq!(\n        loaded.run_no_limits(vec![MontyObject::Int(21)]).unwrap(),\n        MontyObject::Int(42)\n    );\n}\n\n// === RunProgress dump/load Tests ===\n\n#[test]\nfn run_progress_dump_load_roundtrip() {\n    // Start execution with an external function, dump at the call, load and resume\n    let runner = MontyRun::new(\"ext_fn(42) + 1\".to_owned(), \"test.py\", vec![]).unwrap();\n\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    // First resolve the NameLookup for ext_fn\n    let progress = resolve_name_lookups(progress).unwrap();\n\n    // Dump the progress at the external call\n    let bytes = progress.dump().unwrap();\n\n    // Load it back\n    let loaded: RunProgress<NoLimitTracker> = RunProgress::load(&bytes).unwrap();\n\n    // Should still be at the external function call\n    let call = loaded.into_function_call().expect(\"should be at function call\");\n    assert_eq!(call.function_name, \"ext_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(42)]);\n\n    // Resume execution with a return value\n    let result = call.resume(MontyObject::Int(100), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(101)); // 100 + 1\n}\n\n#[test]\nfn run_progress_dump_load_multiple_calls() {\n    // Test multiple external calls with dump/load between each\n    let runner = MontyRun::new(\"x = ext_fn(1); y = ext_fn(2); x + y\".to_owned(), \"test.py\", vec![]).unwrap();\n\n    // First call - resolve NameLookup for ext_fn first\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n    let progress = resolve_name_lookups(progress).unwrap();\n    let bytes = progress.dump().unwrap();\n    let loaded: RunProgress<NoLimitTracker> = RunProgress::load(&bytes).unwrap();\n    let call = loaded.into_function_call().unwrap();\n    assert_eq!(call.function_name, \"ext_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(1)]);\n\n    // Resume first call\n    let progress = call.resume(MontyObject::Int(10), PrintWriter::Stdout).unwrap();\n    // Resolve any NameLookup for the second ext_fn reference\n    let progress = resolve_name_lookups(progress).unwrap();\n\n    // Dump/load at second call\n    let bytes = progress.dump().unwrap();\n    let loaded: RunProgress<NoLimitTracker> = RunProgress::load(&bytes).unwrap();\n    let call = loaded.into_function_call().unwrap();\n    assert_eq!(call.function_name, \"ext_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(2)]);\n\n    // Resume second call to completion\n    let result = call.resume(MontyObject::Int(20), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(30)); // 10 + 20\n}\n\n#[test]\nfn run_progress_complete_roundtrip() {\n    // When execution completes, we can still dump/load the Complete variant\n    let runner = MontyRun::new(\"1 + 2\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let bytes = progress.dump().unwrap();\n    let loaded: RunProgress<NoLimitTracker> = RunProgress::load(&bytes).unwrap();\n\n    assert_eq!(loaded.into_complete().unwrap(), MontyObject::Int(3));\n}\n"
  },
  {
    "path": "crates/monty/tests/bytecode_limits.rs",
    "content": "//! Tests for bytecode operand overflow limits.\n//!\n//! These tests verify that the bytecode compiler handles cases where operands\n//! exceed the u8/u16 limits of the bytecode encoding:\n//!\n//! - Local variable slots: Use wide instructions (u16), so up to 65535 locals work\n//! - Function call arguments: Limited to 255 (u8 operand) - returns SyntaxError if exceeded\n//! - Keyword argument counts: Limited to 255 (u8 operand) - returns SyntaxError if exceeded\n\nuse std::fmt::Write;\n\nuse monty::{ExcType, MontyRun};\n\n/// Generates Python code with N local variables in a function.\n///\n/// Creates: `def f(): v0=0; v1=1; ...; v{n-1}={n-1}; return v{n-1}`\nfn generate_many_locals(count: usize) -> String {\n    let mut code = String::from(\"def f():\\n\");\n    for i in 0..count {\n        writeln!(code, \"    v{i} = {i}\").unwrap();\n    }\n    writeln!(code, \"    return v{}\", count - 1).unwrap();\n    code.push_str(\"f()\");\n    code\n}\n\n/// Generates Python code calling a function with N positional arguments.\n///\n/// Creates: `def f(*args): return len(args)\\nf(0, 1, 2, ..., n-1)`\nfn generate_many_positional_args(count: usize) -> String {\n    let mut code = String::from(\"def f(*args): return len(args)\\nf(\");\n    for i in 0..count {\n        if i > 0 {\n            code.push_str(\", \");\n        }\n        code.push_str(&i.to_string());\n    }\n    code.push(')');\n    code\n}\n\n/// Generates Python code calling a function with N keyword arguments.\n///\n/// Creates: `def f(**kw): return len(kw)\\nf(k0=0, k1=1, ..., k{n-1}={n-1})`\nfn generate_many_keyword_args(count: usize) -> String {\n    let mut code = String::from(\"def f(**kw): return len(kw)\\nf(\");\n    for i in 0..count {\n        if i > 0 {\n            code.push_str(\", \");\n        }\n        write!(code, \"k{i}={i}\").unwrap();\n    }\n    code.push(')');\n    code\n}\n\n/// Generates Python code with a function that has N parameters.\n///\n/// Creates: `def f(p0, p1, ..., p{n-1}): return p{n-1}\\nf(0, 1, ..., n-1)`\nfn generate_many_parameters(count: usize) -> String {\n    let mut code = String::from(\"def f(\");\n    for i in 0..count {\n        if i > 0 {\n            code.push_str(\", \");\n        }\n        write!(code, \"p{i}\").unwrap();\n    }\n    code.push_str(\"):\\n\");\n    writeln!(code, \"    return p{}\", count - 1).unwrap();\n    code.push_str(\"f(\");\n    for i in 0..count {\n        if i > 0 {\n            code.push_str(\", \");\n        }\n        code.push_str(&i.to_string());\n    }\n    code.push(')');\n    code\n}\n\n/// Asserts that a MontyRun result is a SyntaxError with a message containing the expected text.\nfn assert_syntax_error(result: Result<MontyRun, monty::MontyException>, expected_msg: &str) {\n    let err = result.expect_err(\"expected SyntaxError\");\n    assert_eq!(\n        err.exc_type(),\n        ExcType::SyntaxError,\n        \"expected SyntaxError, got {:?}: {:?}\",\n        err.exc_type(),\n        err.message()\n    );\n    let msg = err.message().expect(\"SyntaxError should have message\");\n    assert!(\n        msg.contains(expected_msg),\n        \"expected message containing '{expected_msg}', got: {msg}\"\n    );\n}\n\nmod local_variable_limits {\n    use super::*;\n\n    #[test]\n    fn locals_under_u8_limit_succeeds() {\n        // 255 locals should work with u8 slots (0-254)\n        let code = generate_many_locals(255);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(result.is_ok(), \"255 locals should compile successfully\");\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"255 locals should run successfully\");\n    }\n\n    #[test]\n    fn locals_at_u8_boundary_succeeds() {\n        // 256 locals (slots 0-255) - uses wide instructions for slot 255+\n        let code = generate_many_locals(256);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(\n            result.is_ok(),\n            \"256 locals should compile successfully (wide instructions)\"\n        );\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"256 locals should run successfully\");\n    }\n\n    #[test]\n    fn locals_exceeding_u8_uses_wide_instructions() {\n        // 257 locals requires LoadLocalW/StoreLocalW for slot 256\n        let code = generate_many_locals(257);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(result.is_ok(), \"257 locals should compile (using wide instructions)\");\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"257 locals should run correctly with wide instructions\");\n    }\n\n    #[test]\n    fn locals_well_over_u8_limit() {\n        // 300 locals - well into wide instruction territory\n        let code = generate_many_locals(300);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(result.is_ok(), \"300 locals should compile successfully\");\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"300 locals should run successfully\");\n    }\n}\n\nmod function_argument_limits {\n    use super::*;\n\n    #[test]\n    fn positional_args_under_u8_limit_succeeds() {\n        // 255 positional args should work\n        let code = generate_many_positional_args(255);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(result.is_ok(), \"255 positional args should compile successfully\");\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"255 positional args should run successfully\");\n    }\n\n    #[test]\n    fn positional_args_at_u8_boundary_returns_syntax_error() {\n        // 256 positional args - exceeds u8 limit, should return SyntaxError\n        let code = generate_many_positional_args(256);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert_syntax_error(result, \"more than 255 positional arguments\");\n    }\n\n    #[test]\n    fn positional_args_exceeding_u8_limit_returns_syntax_error() {\n        // 257 positional args - clearly exceeds u8 capacity\n        let code = generate_many_positional_args(257);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert_syntax_error(result, \"more than 255 positional arguments\");\n    }\n}\n\nmod keyword_argument_limits {\n    use super::*;\n\n    #[test]\n    fn keyword_args_under_u8_limit_succeeds() {\n        // 255 keyword args should work\n        let code = generate_many_keyword_args(255);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(result.is_ok(), \"255 keyword args should compile successfully\");\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"255 keyword args should run successfully\");\n    }\n\n    #[test]\n    fn keyword_args_at_u8_boundary_returns_syntax_error() {\n        // 256 keyword args - exceeds u8 limit, should return SyntaxError\n        let code = generate_many_keyword_args(256);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert_syntax_error(result, \"more than 255 keyword arguments\");\n    }\n\n    #[test]\n    fn keyword_args_exceeding_u8_limit_returns_syntax_error() {\n        // 257 keyword args - clearly exceeds u8 capacity\n        let code = generate_many_keyword_args(257);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert_syntax_error(result, \"more than 255 keyword arguments\");\n    }\n}\n\nmod function_parameter_limits {\n    use super::*;\n\n    #[test]\n    fn parameters_under_u8_limit_succeeds() {\n        // 255 parameters should work - both definition and call\n        let code = generate_many_parameters(255);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert!(result.is_ok(), \"255 parameters should compile successfully\");\n\n        let run = result.unwrap();\n        let result = run.run_no_limits(vec![]);\n        assert!(result.is_ok(), \"255 parameters should run successfully\");\n    }\n\n    #[test]\n    fn parameters_at_u8_boundary_returns_syntax_error_for_call() {\n        // 256 parameters - the function definition uses locals (wide instructions ok),\n        // but the call site has 256 positional args which exceeds the limit\n        let code = generate_many_parameters(256);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert_syntax_error(result, \"more than 255 positional arguments\");\n    }\n\n    #[test]\n    fn parameters_exceeding_u8_limit_returns_syntax_error_for_call() {\n        // 257 parameters - same issue, call site has too many args\n        let code = generate_many_parameters(257);\n        let result = MontyRun::new(code, \"test.py\", vec![]);\n        assert_syntax_error(result, \"more than 255 positional arguments\");\n    }\n}\n"
  },
  {
    "path": "crates/monty/tests/datatest_runner.rs",
    "content": "use std::{\n    cell::RefCell,\n    collections::{HashMap, HashSet},\n    error::Error,\n    ffi::CString,\n    fs,\n    panic::{self, AssertUnwindSafe},\n    path::Path,\n    sync::{\n        OnceLock,\n        mpsc::{self, RecvTimeoutError},\n    },\n    thread,\n    time::Duration,\n};\n\nuse ahash::AHashMap;\nuse monty::{\n    ExcType, ExtFunctionResult, LimitedTracker, MontyException, MontyObject, MontyRun, NameLookupResult, OsFunction,\n    PrintWriter, ResourceLimits, RunProgress, dir_stat, file_stat,\n};\nuse pyo3::{prelude::*, types::PyDict};\nuse similar::TextDiff;\n\n/// Recursion limit for test execution.\n///\n/// Used for both Monty and CPython tests. CPython needs ~5 extra frames\n/// for runpy overhead, which is added in run_file_and_get_traceback.\n///\n/// NOTE this value is chosen to avoid both:\n/// * other recursion errors in python (if it's too low)\n/// * and, stack overflows in debug rust (if it's too high)\nconst TEST_RECURSION_LIMIT: usize = 50;\n\n/// Test configuration parsed from directive comments.\n///\n/// Parsed from an optional first-line comment like `# xfail=monty,cpython` or `# call-external`.\n/// If not present, defaults to running on both interpreters in standard mode.\n///\n/// ## Xfail Semantics (Strict)\n/// - `xfail=monty` - Test is expected to fail on Monty; if it passes, that's an error\n/// - `xfail=cpython` - Test is expected to fail on CPython; if it passes, that's an error\n/// - `xfail=monty,cpython` - Expected to fail on both interpreters\n#[derive(Debug, Clone, Default)]\n#[expect(clippy::struct_excessive_bools)]\nstruct TestConfig {\n    /// When true, test is expected to fail on Monty (strict xfail).\n    xfail_monty: bool,\n    /// When true, test is expected to fail on CPython (strict xfail).\n    xfail_cpython: bool,\n    /// When true, use MontyRun with external function support instead of MontyRun.\n    iter_mode: bool,\n    /// When true, wrap code in async context for CPython execution.\n    /// Used for tests with top-level await which Monty supports but CPython doesn't.\n    async_mode: bool,\n}\n\n/// Represents the expected outcome of a test fixture\n#[derive(Debug, Clone)]\nenum Expectation {\n    /// Expect exception (parse-time or runtime) with specific message\n    Raise(String),\n    /// Expect successful execution, check py_str() output\n    ReturnStr(String),\n    /// Expect successful execution, check py_repr() output\n    Return(String),\n    /// Expect successful execution, check py_type() output\n    ReturnType(String),\n    /// Expect successful execution, check ref counts of named variables.\n    /// Only used when `ref-count-return` feature is enabled; skipped otherwise.\n    RefCounts(#[cfg_attr(not(feature = \"ref-count-return\"), expect(dead_code))] AHashMap<String, usize>),\n    /// Expect exception with full traceback comparison.\n    /// The expected traceback string should match exactly between Monty and CPython.\n    Traceback(String),\n    /// Expect successful execution without raising an exception (no return value check).\n    /// Used for tests that rely on asserts or just verify code runs.\n    NoException,\n}\n\nimpl Expectation {\n    /// Returns the expected value string\n    fn expected_value(&self) -> &str {\n        match self {\n            Self::Raise(s) | Self::ReturnStr(s) | Self::Return(s) | Self::ReturnType(s) | Self::Traceback(s) => s,\n            Self::RefCounts(_) | Self::NoException => \"\",\n        }\n    }\n}\n\n/// Parse a Python fixture file into code, expected outcome, and test configuration.\n///\n/// The file may optionally contain a `# xfail=monty,cpython` comment to specify\n/// which interpreters the test is expected to fail on. If not present, defaults to\n/// running on both and expecting success.\n///\n/// The file may have an expectation comment as the LAST line:\n/// - `# Raise=ExceptionType('message')` - Exception (parse-time or runtime)\n/// - `# Return.str=value` - Check py_str() output\n/// - `# Return=value` - Check py_repr() output\n/// - `# Return.type=typename` - Check py_type() output\n/// - `# ref-counts={'var': count, ...}` - Check ref counts of named heap variables\n///\n/// Or a traceback expectation as a triple-quoted string at the end (uses actual test filename):\n/// ```text\n/// \"\"\"TRACEBACK:\n/// Traceback (most recent call last):\n///   File \"my_test.py\", line 4, in <module>\n///     foo()\n/// ValueError: message\n/// \"\"\"\n/// ```\n///\n/// If no expectation comment is present, the test just verifies the code runs without exception.\nfn parse_fixture(content: &str) -> (String, Expectation, TestConfig) {\n    let lines: Vec<&str> = content.lines().collect();\n\n    assert!(!lines.is_empty(), \"Empty fixture file\");\n\n    // comment lines with leading # and spaces stripped\n    let comment_lines = lines\n        .iter()\n        .filter(|line| line.starts_with('#'))\n        .map(|line| line.trim_start_matches('#').trim())\n        .collect::<Vec<_>>();\n\n    let mut config = TestConfig {\n        iter_mode: comment_lines.iter().any(|line| line.starts_with(\"call-external\")),\n        async_mode: comment_lines.iter().any(|line| line.starts_with(\"run-async\")),\n        ..Default::default()\n    };\n    // Check for \"xfail=\" directive\n    if let Some(&xfail_line) = comment_lines.iter().find(|line| line.starts_with(\"xfail=\")) {\n        // Parse until whitespace or end of line\n        let xfail_end = xfail_line.find(|c: char| c.is_whitespace()).unwrap_or(xfail_line.len());\n        let xfail_str = &xfail_line[..xfail_end];\n        config.xfail_monty = xfail_str.contains(\"monty\");\n        config.xfail_cpython = xfail_str.contains(\"cpython\");\n    }\n\n    // Check for TRACEBACK expectation (triple-quoted string at end of file)\n    // Format: \"\"\"TRACEBACK:\\n...\\n\"\"\"\n    if let Some((code, traceback)) = parse_traceback_expectation(content) {\n        return (code, Expectation::Traceback(traceback), config);\n    }\n\n    // Get the last line and check if it's an expectation comment\n    let last_line = lines.last().unwrap();\n\n    // Parse expectation from comment line if present\n    // Note: Check more specific patterns first (Return.str, Return.type, ref-counts) before general Return\n    let (expectation, code_lines) = if let Some(expected) = last_line.strip_prefix(\"# ref-counts=\") {\n        (\n            Expectation::RefCounts(parse_ref_counts(expected)),\n            &lines[..lines.len() - 1],\n        )\n    } else if let Some(expected) = last_line.strip_prefix(\"# Return.str=\") {\n        (Expectation::ReturnStr(expected.to_string()), &lines[..lines.len() - 1])\n    } else if let Some(expected) = last_line.strip_prefix(\"# Return.type=\") {\n        (Expectation::ReturnType(expected.to_string()), &lines[..lines.len() - 1])\n    } else if let Some(expected) = last_line.strip_prefix(\"# Return=\") {\n        (Expectation::Return(expected.to_string()), &lines[..lines.len() - 1])\n    } else if let Some(expected) = last_line.strip_prefix(\"# Raise=\") {\n        (Expectation::Raise(expected.to_string()), &lines[..lines.len() - 1])\n    } else {\n        // No expectation comment - just run and check it doesn't raise\n        (Expectation::NoException, &lines[..])\n    };\n\n    // Code is everything except the directive comment (and expectation comment if present)\n    let code = code_lines.join(\"\\n\");\n\n    (code, expectation, config)\n}\n\n/// Parses a TRACEBACK expectation from the end of a fixture file.\n///\n/// Looks for a triple-quoted string starting with `\"\"\"TRACEBACK:` at the end of the file.\n/// Returns `Some((code, expected_traceback))` if found, `None` otherwise.\n///\n/// The traceback string should contain the full expected output including the\n/// \"Traceback (most recent call last):\" header and the exception line.\nfn parse_traceback_expectation(content: &str) -> Option<(String, String)> {\n    // Format: \"\"\"\\nTRACEBACK:\\n...\\n\"\"\"\n    const MARKER: &str = \"\\\"\\\"\\\"\\nTRACEBACK:\\n\";\n\n    // Find the TRACEBACK marker\n    let marker_pos = content.find(MARKER)?;\n\n    // Extract the code before the marker\n    let code_part = &content[..marker_pos];\n    let lines: Vec<&str> = code_part.lines().collect();\n    let code = lines.join(\"\\n\").trim_end().to_string();\n\n    // Extract the traceback content between the markers\n    let after_marker = &content[marker_pos + MARKER.len()..];\n\n    // Find the closing triple quotes (preceded by newline)\n    let end_pos = after_marker.find(\"\\n\\\"\\\"\\\"\")?;\n    let traceback_content = &after_marker[..end_pos];\n\n    Some((code, traceback_content.to_string()))\n}\n\n/// Parses the ref-counts format: {'var': count, 'var2': count2}\n///\n/// Supports both single and double quotes for variable names.\n/// Example: {'x': 2, 'y': 1} or {\"x\": 2, \"y\": 1}\nfn parse_ref_counts(s: &str) -> AHashMap<String, usize> {\n    let mut counts = AHashMap::new();\n    let trimmed = s.trim().trim_start_matches('{').trim_end_matches('}');\n    for pair in trimmed.split(',') {\n        let pair = pair.trim();\n        if pair.is_empty() {\n            continue;\n        }\n        let parts: Vec<&str> = pair.split(':').collect();\n        assert!(\n            parts.len() == 2,\n            \"Invalid ref-counts pair format: {pair}. Expected 'name': count\"\n        );\n        let name = parts[0].trim().trim_matches('\\'').trim_matches('\"');\n        let count: usize = parts[1]\n            .trim()\n            .parse()\n            .unwrap_or_else(|_| panic!(\"Invalid ref count value: {}\", parts[1]));\n        counts.insert(name.to_string(), count);\n    }\n    counts\n}\n\n/// Python implementations of external functions for running iter mode tests in CPython.\n///\n/// These implementations mirror the behavior of `dispatch_external_call` so that\n/// iter mode tests produce identical results in both Monty and CPython.\n///\n/// This is loaded from `scripts/iter_test_methods.py` which is also imported by\n/// `scripts/run_traceback.py` to ensure consistency.\nconst ITER_EXT_FUNCTIONS_PYTHON: &str = include_str!(\"../../../scripts/iter_test_methods.py\");\n\n/// Pre-imports Python modules that can cause race conditions during parallel test execution.\n///\n/// Python's import machinery isn't fully thread-safe during module initialization.\n/// When multiple tests try to import modules like `typing` or `dataclasses` simultaneously,\n/// one thread may see a partially initialized module, causing `AttributeError`.\n///\n/// This function must be called once before any parallel test execution to ensure\n/// all relevant modules are fully initialized.\nfn ensure_python_modules_imported() {\n    static INIT: OnceLock<()> = OnceLock::new();\n    INIT.get_or_init(|| {\n        Python::attach(|py| {\n            // Import modules that are used by iter_test_methods.py and can cause race conditions.\n            // The order matters: import dependencies first.\n            py.import(\"typing\").expect(\"Failed to import typing\");\n            py.import(\"dataclasses\").expect(\"Failed to import dataclasses\");\n            py.import(\"pathlib\").expect(\"Failed to import pathlib\");\n            py.import(\"stat\").expect(\"Failed to import stat\");\n            py.import(\"asyncio\").expect(\"Failed to import asyncio\");\n            py.import(\"traceback\").expect(\"Failed to import traceback\");\n\n            // Also pre-execute the iter_test_methods code once to ensure all its\n            // module-level code (dataclass definitions, monkey-patches) is initialized\n            let ext_funcs_cstr = CString::new(ITER_EXT_FUNCTIONS_PYTHON).expect(\"Invalid C string\");\n            py.run(&ext_funcs_cstr, None, None)\n                .expect(\"Failed to pre-initialize iter_test_methods\");\n        });\n    });\n}\n\n/// Result from dispatching an external function call.\n///\n/// Distinguishes between synchronous calls (return immediately) and\n/// asynchronous calls (return a future that needs later resolution).\nenum DispatchResult {\n    /// Synchronous result - pass directly to `state.run()`.\n    Sync(ExtFunctionResult),\n    /// Asynchronous call - use `state.run_pending()` and resolve later.\n    /// Contains the value to resolve the future with.\n    Async(MontyObject),\n}\n\n/// Dispatches an external function call to the appropriate test implementation.\n///\n/// Returns `DispatchResult::Sync` for synchronous calls or `DispatchResult::Async`\n/// for coroutine calls that should use `run_pending()`.\n///\n/// # Panics\n/// Panics if the function name is unknown or arguments are invalid types.\nfn dispatch_external_call(name: &str, args: Vec<MontyObject>) -> DispatchResult {\n    match name {\n        \"add_ints\" => {\n            assert!(args.len() == 2, \"add_ints requires 2 arguments\");\n            let a = i64::try_from(&args[0]).expect(\"add_ints: first arg must be int\");\n            let b = i64::try_from(&args[1]).expect(\"add_ints: second arg must be int\");\n            DispatchResult::Sync(MontyObject::Int(a + b).into())\n        }\n        \"concat_strings\" => {\n            assert!(args.len() == 2, \"concat_strings requires 2 arguments\");\n            let a = String::try_from(&args[0]).expect(\"concat_strings: first arg must be str\");\n            let b = String::try_from(&args[1]).expect(\"concat_strings: second arg must be str\");\n            DispatchResult::Sync(MontyObject::String(a + &b).into())\n        }\n        \"return_value\" => {\n            assert!(args.len() == 1, \"return_value requires 1 argument\");\n            DispatchResult::Sync(args.into_iter().next().unwrap().into())\n        }\n        \"get_list\" => {\n            assert!(args.is_empty(), \"get_list requires no arguments\");\n            DispatchResult::Sync(\n                MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2), MontyObject::Int(3)]).into(),\n            )\n        }\n        \"raise_error\" => {\n            // raise_error(exc_type: str, message: str) -> raises exception\n            assert!(args.len() == 2, \"raise_error requires 2 arguments\");\n            let exc_type_str = String::try_from(&args[0]).expect(\"raise_error: first arg must be str\");\n            let message = String::try_from(&args[1]).expect(\"raise_error: second arg must be str\");\n            let exc_type = match exc_type_str.as_str() {\n                \"ValueError\" => ExcType::ValueError,\n                \"TypeError\" => ExcType::TypeError,\n                \"KeyError\" => ExcType::KeyError,\n                \"RuntimeError\" => ExcType::RuntimeError,\n                _ => panic!(\"raise_error: unsupported exception type: {exc_type_str}\"),\n            };\n            DispatchResult::Sync(MontyException::new(exc_type, Some(message)).into())\n        }\n        \"make_point\" => {\n            assert!(args.is_empty(), \"make_point requires no arguments\");\n            // Return an immutable Point(x=1, y=2) dataclass\n            DispatchResult::Sync(\n                MontyObject::Dataclass {\n                    name: \"Point\".to_string(),\n                    type_id: 0, // Test fixture has no real Python type\n                    field_names: vec![\"x\".to_string(), \"y\".to_string()],\n                    attrs: vec![\n                        (MontyObject::String(\"x\".to_string()), MontyObject::Int(1)),\n                        (MontyObject::String(\"y\".to_string()), MontyObject::Int(2)),\n                    ]\n                    .into(),\n\n                    frozen: true,\n                }\n                .into(),\n            )\n        }\n        \"make_mutable_point\" => {\n            assert!(args.is_empty(), \"make_mutable_point requires no arguments\");\n            // Return a mutable Point(x=1, y=2) dataclass\n            DispatchResult::Sync(\n                MontyObject::Dataclass {\n                    name: \"MutablePoint\".to_string(),\n                    type_id: 0, // Test fixture has no real Python type\n                    field_names: vec![\"x\".to_string(), \"y\".to_string()],\n                    attrs: vec![\n                        (MontyObject::String(\"x\".to_string()), MontyObject::Int(1)),\n                        (MontyObject::String(\"y\".to_string()), MontyObject::Int(2)),\n                    ]\n                    .into(),\n\n                    frozen: false,\n                }\n                .into(),\n            )\n        }\n        \"make_user\" => {\n            assert!(args.len() == 1, \"make_user requires 1 argument\");\n            let name = String::try_from(&args[0]).expect(\"make_user: first arg must be str\");\n            // Return an immutable User(name=name, active=True) dataclass\n            DispatchResult::Sync(\n                MontyObject::Dataclass {\n                    name: \"User\".to_string(),\n                    type_id: 0, // Test fixture has no real Python type\n                    field_names: vec![\"name\".to_string(), \"active\".to_string()],\n                    attrs: vec![\n                        (MontyObject::String(\"name\".to_string()), MontyObject::String(name)),\n                        (MontyObject::String(\"active\".to_string()), MontyObject::Bool(true)),\n                    ]\n                    .into(),\n\n                    frozen: true,\n                }\n                .into(),\n            )\n        }\n        \"make_empty\" => {\n            assert!(args.is_empty(), \"make_empty requires no arguments\");\n            // Return an immutable empty dataclass with no fields\n            DispatchResult::Sync(\n                MontyObject::Dataclass {\n                    name: \"Empty\".to_string(),\n                    type_id: 0, // Test fixture has no real Python type\n                    field_names: vec![],\n                    attrs: vec![].into(),\n\n                    frozen: true,\n                }\n                .into(),\n            )\n        }\n        \"async_call\" => {\n            // async_call(x) -> coroutine that returns x\n            // This is an async function - use run_pending() and resolve later\n            assert!(args.len() == 1, \"async_call requires 1 argument\");\n            DispatchResult::Async(args.into_iter().next().unwrap())\n        }\n        _ => panic!(\"Unknown external function: {name}\"),\n    }\n}\n\n/// Dispatches a dataclass method call to the appropriate test implementation.\n///\n/// The first argument is always the dataclass instance (`self`). Known methods\n/// are implemented to mirror the Python dataclass methods in `iter_test_methods.py`.\n/// Unknown methods return `AttributeError`.\nfn dispatch_method_call(\n    method_name: &str,\n    args: &[MontyObject],\n    kwargs: &[(MontyObject, MontyObject)],\n) -> ExtFunctionResult {\n    let class_name = match args.first() {\n        Some(MontyObject::Dataclass { name, .. }) => name.as_str(),\n        _ => \"<unknown>\",\n    };\n\n    match (class_name, method_name) {\n        // Point.sum(self) -> int\n        (\"Point\" | \"MutablePoint\", \"sum\") => {\n            let (x, y) = extract_point_fields(&args[0]);\n            MontyObject::Int(x + y).into()\n        }\n        // Point.add(self, dx, dy) -> Point\n        (\"Point\", \"add\") => {\n            assert!(args.len() == 3, \"Point.add requires self, dx, dy\");\n            let (x, y) = extract_point_fields(&args[0]);\n            let dx = i64::try_from(&args[1]).expect(\"dx must be int\");\n            let dy = i64::try_from(&args[2]).expect(\"dy must be int\");\n            MontyObject::Dataclass {\n                name: \"Point\".to_string(),\n                type_id: 0,\n                field_names: vec![\"x\".to_string(), \"y\".to_string()],\n                attrs: vec![\n                    (MontyObject::String(\"x\".to_string()), MontyObject::Int(x + dx)),\n                    (MontyObject::String(\"y\".to_string()), MontyObject::Int(y + dy)),\n                ]\n                .into(),\n                frozen: true,\n            }\n            .into()\n        }\n        // Point.scale(self, factor) -> Point\n        (\"Point\", \"scale\") => {\n            assert!(args.len() == 2, \"Point.scale requires self, factor\");\n            let (x, y) = extract_point_fields(&args[0]);\n            let factor = i64::try_from(&args[1]).expect(\"factor must be int\");\n            MontyObject::Dataclass {\n                name: \"Point\".to_string(),\n                type_id: 0,\n                field_names: vec![\"x\".to_string(), \"y\".to_string()],\n                attrs: vec![\n                    (MontyObject::String(\"x\".to_string()), MontyObject::Int(x * factor)),\n                    (MontyObject::String(\"y\".to_string()), MontyObject::Int(y * factor)),\n                ]\n                .into(),\n                frozen: true,\n            }\n            .into()\n        }\n        // Point.describe(self, label='point') -> str\n        (\"Point\", \"describe\") => {\n            let (x, y) = extract_point_fields(&args[0]);\n            // Check positional arg first, then kwargs, then default\n            let label = if args.len() > 1 {\n                String::try_from(&args[1]).expect(\"label must be str\")\n            } else if let Some(kw_label) = get_kwarg_str(kwargs, \"label\") {\n                kw_label\n            } else {\n                \"point\".to_string()\n            };\n            MontyObject::String(format!(\"{label}({x}, {y})\")).into()\n        }\n        // MutablePoint.shift(self, dx, dy) -> None (mutates in-place via host)\n        // Note: In the test runner, we can't actually mutate the dataclass in-place\n        // since the host doesn't have direct heap access. Return None as the method\n        // would in Python (the mutation happens inside Python's method body).\n        // For test coverage purposes, we just return None.\n        (\"MutablePoint\", \"shift\") => MontyObject::None.into(),\n        // User.greeting(self) -> str\n        (\"User\", \"greeting\") => {\n            let name = extract_user_name(&args[0]);\n            MontyObject::String(format!(\"Hello, {name}!\")).into()\n        }\n        // Unknown method — return AttributeError\n        _ => {\n            let message = format!(\"'{class_name}' object has no attribute '{method_name}'\");\n            MontyException::new(ExcType::AttributeError, Some(message)).into()\n        }\n    }\n}\n\n/// Extracts (x, y) fields from a Point or MutablePoint `MontyObject::Dataclass`.\nfn extract_point_fields(obj: &MontyObject) -> (i64, i64) {\n    match obj {\n        MontyObject::Dataclass { attrs, .. } => {\n            let mut x = 0i64;\n            let mut y = 0i64;\n            for (key, value) in attrs {\n                if let MontyObject::String(k) = key {\n                    match k.as_str() {\n                        \"x\" => x = i64::try_from(value).expect(\"x must be int\"),\n                        \"y\" => y = i64::try_from(value).expect(\"y must be int\"),\n                        _ => {}\n                    }\n                }\n            }\n            (x, y)\n        }\n        other => panic!(\"Expected Dataclass, got {other:?}\"),\n    }\n}\n\n/// Extracts a string kwarg value by key name.\nfn get_kwarg_str(kwargs: &[(MontyObject, MontyObject)], name: &str) -> Option<String> {\n    for (key, value) in kwargs {\n        if let MontyObject::String(key_str) = key\n            && key_str == name\n        {\n            return Some(String::try_from(value).expect(\"kwarg value must be str\"));\n        }\n    }\n    None\n}\n\n/// Extracts the `name` field from a User `MontyObject::Dataclass`.\nfn extract_user_name(obj: &MontyObject) -> String {\n    match obj {\n        MontyObject::Dataclass { attrs, .. } => {\n            for (key, value) in attrs {\n                if let MontyObject::String(k) = key\n                    && k == \"name\"\n                {\n                    return String::try_from(value).expect(\"name must be str\");\n                }\n            }\n            panic!(\"User dataclass has no 'name' field\");\n        }\n        other => panic!(\"Expected Dataclass, got {other:?}\"),\n    }\n}\n\n// =============================================================================\n// Virtual Filesystem for OS Call Tests\n// =============================================================================\n\n/// Virtual file entry for OS call tests (static VFS).\nstruct StaticVirtualFile {\n    content: &'static [u8],\n    mode: i64,\n}\n\n/// Virtual file entry (owned, for unified VFS lookups).\nstruct VirtualFile {\n    content: Vec<u8>,\n    mode: i64,\n}\n\n/// Virtual filesystem modification time (arbitrary fixed timestamp).\nconst VFS_MTIME: f64 = 1_700_000_000.0;\n\n/// Virtual filesystem for testing Path methods.\n///\n/// Structure:\n/// ```text\n/// /virtual/\n/// ├── file.txt           (file, 644, \"hello world\\n\")\n/// ├── data.bin           (file, 644, b\"\\x00\\x01\\x02\\x03\")\n/// ├── empty.txt          (file, 644, \"\")\n/// ├── subdir/\n/// │   ├── nested.txt     (file, 644, \"nested content\")\n/// │   └── deep/\n/// │       └── file.txt   (file, 644, \"deep\")\n/// └── readonly.txt       (file, 444, \"readonly\")\n///\n/// /nonexistent           (does not exist)\n/// ```\nfn get_static_virtual_file(path: &str) -> Option<StaticVirtualFile> {\n    match path {\n        \"/virtual/file.txt\" => Some(StaticVirtualFile {\n            content: b\"hello world\\n\",\n            mode: 0o644,\n        }),\n        \"/virtual/data.bin\" => Some(StaticVirtualFile {\n            content: b\"\\x00\\x01\\x02\\x03\",\n            mode: 0o644,\n        }),\n        \"/virtual/empty.txt\" => Some(StaticVirtualFile {\n            content: b\"\",\n            mode: 0o644,\n        }),\n        \"/virtual/subdir/nested.txt\" => Some(StaticVirtualFile {\n            content: b\"nested content\",\n            mode: 0o644,\n        }),\n        \"/virtual/subdir/deep/file.txt\" => Some(StaticVirtualFile {\n            content: b\"deep\",\n            mode: 0o644,\n        }),\n        \"/virtual/readonly.txt\" => Some(StaticVirtualFile {\n            content: b\"readonly\",\n            mode: 0o444,\n        }),\n        _ => None,\n    }\n}\n\n/// Gets a virtual file, checking the mutable layer first, then falling back to static.\nfn get_virtual_file(path: &str) -> Option<VirtualFile> {\n    // Check mutable layer first\n    let mutable_result = MUTABLE_VFS.with(|vfs| {\n        let vfs = vfs.borrow();\n        // Check if deleted\n        if vfs.deleted_files.contains(path) {\n            return Some(None);\n        }\n        // Check if exists in mutable layer\n        if let Some((content, mode)) = vfs.files.get(path) {\n            return Some(Some(VirtualFile {\n                content: content.clone(),\n                mode: *mode,\n            }));\n        }\n        None\n    });\n\n    match mutable_result {\n        Some(Some(file)) => Some(file),\n        Some(None) => None, // File was deleted\n        None => {\n            // Fall back to static VFS\n            get_static_virtual_file(path).map(|f| VirtualFile {\n                content: f.content.to_vec(),\n                mode: f.mode,\n            })\n        }\n    }\n}\n\n// =============================================================================\n// Mutable VFS Layer (Thread-Local Storage for Write Operations)\n// =============================================================================\n\n/// Mutable state for the virtual filesystem, supporting write operations.\n///\n/// This layer sits on top of the static VFS and allows tests to create, modify, and\n/// delete files and directories. The state is thread-local so tests don't interfere\n/// with each other.\n#[derive(Default)]\nstruct MutableVfs {\n    /// Files created or modified during test execution.\n    files: HashMap<String, (Vec<u8>, i64)>, // path -> (content, mode)\n    /// Directories created during test execution.\n    dirs: HashSet<String>,\n    /// Files deleted during test execution (shadows static VFS entries).\n    deleted_files: HashSet<String>,\n    /// Directories deleted during test execution.\n    deleted_dirs: HashSet<String>,\n}\n\nthread_local! {\n    /// Thread-local mutable VFS state.\n    static MUTABLE_VFS: RefCell<MutableVfs> = RefCell::new(MutableVfs::default());\n}\n\n/// Resets the mutable VFS state for a new test.\nfn reset_mutable_vfs() {\n    MUTABLE_VFS.with(|vfs| {\n        *vfs.borrow_mut() = MutableVfs::default();\n    });\n}\n\n/// Check if the given path is a directory in the virtual filesystem.\nfn is_virtual_dir(path: &str) -> bool {\n    // Check mutable layer first\n    let result = MUTABLE_VFS.with(|vfs| {\n        let vfs = vfs.borrow();\n        if vfs.deleted_dirs.contains(path) {\n            return Some(false);\n        }\n        if vfs.dirs.contains(path) {\n            return Some(true);\n        }\n        None\n    });\n    if let Some(is_dir) = result {\n        return is_dir;\n    }\n    // Fall back to static VFS\n    matches!(path, \"/virtual\" | \"/virtual/subdir\" | \"/virtual/subdir/deep\")\n}\n\n/// Get directory entries for a virtual directory.\nfn get_virtual_dir_entries(path: &str) -> Option<Vec<String>> {\n    // First check if the directory exists\n    if !is_virtual_dir(path) {\n        return None;\n    }\n\n    // Get static entries (if any)\n    let static_entries: Vec<&'static str> = match path {\n        \"/virtual\" => vec![\n            \"/virtual/file.txt\",\n            \"/virtual/data.bin\",\n            \"/virtual/empty.txt\",\n            \"/virtual/subdir\",\n            \"/virtual/readonly.txt\",\n        ],\n        \"/virtual/subdir\" => vec![\"/virtual/subdir/nested.txt\", \"/virtual/subdir/deep\"],\n        \"/virtual/subdir/deep\" => vec![\"/virtual/subdir/deep/file.txt\"],\n        _ => vec![],\n    };\n\n    // Combine with mutable layer\n    MUTABLE_VFS.with(|vfs| {\n        let vfs = vfs.borrow();\n        let mut entries: HashSet<String> = static_entries\n            .iter()\n            .filter(|e| {\n                let s: &str = e;\n                !vfs.deleted_files.contains(s) && !vfs.deleted_dirs.contains(s)\n            })\n            .map(|e| (*e).to_owned())\n            .collect();\n\n        // Add mutable files and dirs in this directory\n        let prefix = if path.ends_with('/') {\n            path.to_owned()\n        } else {\n            format!(\"{path}/\")\n        };\n        for file_path in vfs.files.keys() {\n            if file_path.starts_with(&prefix) {\n                // Only include direct children (not nested)\n                let rest = &file_path[prefix.len()..];\n                if !rest.contains('/') {\n                    entries.insert(file_path.clone());\n                }\n            }\n        }\n        for dir_path in &vfs.dirs {\n            if dir_path.starts_with(&prefix) {\n                let rest = &dir_path[prefix.len()..];\n                if !rest.contains('/') {\n                    entries.insert(dir_path.clone());\n                }\n            }\n        }\n\n        Some(entries.into_iter().collect())\n    })\n}\n\n/// Helper to get a boolean kwarg by name.\nfn get_kwarg_bool(kwargs: &[(MontyObject, MontyObject)], name: &str) -> bool {\n    for (key, value) in kwargs {\n        if let MontyObject::String(key_str) = key\n            && key_str == name\n        {\n            return matches!(value, MontyObject::Bool(true));\n        }\n    }\n    false\n}\n\n/// Dispatches an OS function call using the virtual filesystem.\n///\n/// Returns an `ExternalResult` to pass back to the Monty interpreter.\n/// Raises `FileNotFoundError` for missing files/directories.\n#[expect(clippy::cast_possible_wrap)] // Virtual file sizes are tiny, no wrap possible\nfn dispatch_os_call(\n    function: OsFunction,\n    args: &[MontyObject],\n    kwargs: &[(MontyObject, MontyObject)],\n) -> ExtFunctionResult {\n    // Handle GetEnviron first as it takes no path argument\n    if function == OsFunction::GetEnviron {\n        // Return the virtual environment as a dict\n        let env_dict = vec![\n            (\n                MontyObject::String(\"VIRTUAL_HOME\".to_owned()),\n                MontyObject::String(\"/virtual/home\".to_owned()),\n            ),\n            (\n                MontyObject::String(\"VIRTUAL_USER\".to_owned()),\n                MontyObject::String(\"testuser\".to_owned()),\n            ),\n            (\n                MontyObject::String(\"VIRTUAL_EMPTY\".to_owned()),\n                MontyObject::String(String::new()),\n            ),\n        ];\n        return MontyObject::Dict(env_dict.into()).into();\n    }\n\n    // Extract path from MontyObject::Path (or String for backwards compatibility)\n    let path = match &args[0] {\n        MontyObject::Path(p) => p.clone(),\n        MontyObject::String(s) => s.clone(),\n        other => panic!(\"OS call: first arg must be path, got {other:?}\"),\n    };\n\n    match function {\n        OsFunction::GetEnviron => unreachable!(\"handled above\"),\n        OsFunction::Exists => {\n            let exists = get_virtual_file(&path).is_some() || is_virtual_dir(&path);\n            MontyObject::Bool(exists).into()\n        }\n        OsFunction::IsFile => {\n            let is_file = get_virtual_file(&path).is_some();\n            MontyObject::Bool(is_file).into()\n        }\n        OsFunction::IsDir => {\n            let is_dir = is_virtual_dir(&path);\n            MontyObject::Bool(is_dir).into()\n        }\n        OsFunction::IsSymlink => {\n            // Virtual filesystem doesn't have symlinks\n            MontyObject::Bool(false).into()\n        }\n        OsFunction::ReadText => {\n            if let Some(file) = get_virtual_file(&path) {\n                match std::str::from_utf8(&file.content) {\n                    Ok(text) => MontyObject::String(text.to_owned()).into(),\n                    Err(_) => MontyException::new(\n                        ExcType::UnicodeDecodeError,\n                        Some(\"'utf-8' codec can't decode bytes\".to_owned()),\n                    )\n                    .into(),\n                }\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n        OsFunction::ReadBytes => {\n            if let Some(file) = get_virtual_file(&path) {\n                MontyObject::Bytes(file.content).into()\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n        OsFunction::Stat => {\n            if let Some(file) = get_virtual_file(&path) {\n                file_stat(file.mode, file.content.len() as i64, VFS_MTIME).into()\n            } else if is_virtual_dir(&path) {\n                dir_stat(0o755, VFS_MTIME).into()\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n        OsFunction::Iterdir => {\n            if let Some(entries) = get_virtual_dir_entries(&path) {\n                // Return Path objects, not strings\n                let list: Vec<MontyObject> = entries.into_iter().map(MontyObject::Path).collect();\n                MontyObject::List(list).into()\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n        OsFunction::Resolve | OsFunction::Absolute => {\n            // For virtual paths, return as-is (they're already absolute)\n            MontyObject::String(path).into()\n        }\n        OsFunction::Getenv => {\n            // Virtual environment for testing os.getenv()\n            // args[0] is key, args[1] is default (may be None)\n            let key = String::try_from(&args[0]).expect(\"getenv: first arg must be key string\");\n            let default = &args[1];\n\n            // Provide a few test environment variables\n            let value = match key.as_str() {\n                \"VIRTUAL_HOME\" => Some(\"/virtual/home\"),\n                \"VIRTUAL_USER\" => Some(\"testuser\"),\n                \"VIRTUAL_EMPTY\" => Some(\"\"),\n                _ => None,\n            };\n\n            if let Some(v) = value {\n                MontyObject::String(v.to_owned()).into()\n            } else if matches!(default, MontyObject::None) {\n                MontyObject::None.into()\n            } else {\n                // Return the default value\n                default.clone().into()\n            }\n        }\n        OsFunction::WriteText => {\n            // args[0] is path, args[1] is text content\n            let text = String::try_from(&args[1]).expect(\"write_text: second arg must be string\");\n            MUTABLE_VFS.with(|vfs| {\n                let mut vfs = vfs.borrow_mut();\n                vfs.files.insert(path.clone(), (text.into_bytes(), 0o644));\n                vfs.deleted_files.remove(&path);\n            });\n            // write_text returns the number of bytes written\n            let byte_count = MUTABLE_VFS.with(|vfs| vfs.borrow().files.get(&path).map_or(0, |(c, _)| c.len()));\n            MontyObject::Int(byte_count as i64).into()\n        }\n        OsFunction::WriteBytes => {\n            // args[0] is path, args[1] is bytes content\n            let bytes = match &args[1] {\n                MontyObject::Bytes(b) => b.clone(),\n                other => panic!(\"write_bytes: second arg must be bytes, got {other:?}\"),\n            };\n            let byte_count = bytes.len();\n            MUTABLE_VFS.with(|vfs| {\n                let mut vfs = vfs.borrow_mut();\n                vfs.files.insert(path.clone(), (bytes, 0o644));\n                vfs.deleted_files.remove(&path);\n            });\n            // write_bytes returns the number of bytes written\n            MontyObject::Int(byte_count as i64).into()\n        }\n        OsFunction::Mkdir => {\n            // Check for parents and exist_ok in kwargs (e.g., mkdir(parents=True, exist_ok=True))\n            let parents = get_kwarg_bool(kwargs, \"parents\");\n            let exist_ok = get_kwarg_bool(kwargs, \"exist_ok\");\n\n            // Check if already exists\n            if is_virtual_dir(&path) {\n                if exist_ok {\n                    return MontyObject::None.into();\n                }\n                return MontyException::new(ExcType::OSError, Some(format!(\"[Errno 17] File exists: '{path}'\"))).into();\n            }\n\n            // Check parent directory\n            let parent = std::path::Path::new(&path)\n                .parent()\n                .map(|p| p.to_string_lossy().to_string())\n                .unwrap_or_default();\n            if !parent.is_empty() && !is_virtual_dir(&parent) {\n                if parents {\n                    // Create parent directories recursively\n                    create_parent_dirs(&parent);\n                } else {\n                    return MontyException::new(\n                        ExcType::FileNotFoundError,\n                        Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                    )\n                    .into();\n                }\n            }\n\n            MUTABLE_VFS.with(|vfs| {\n                let mut vfs = vfs.borrow_mut();\n                vfs.deleted_dirs.remove(&path);\n                vfs.dirs.insert(path);\n            });\n            MontyObject::None.into()\n        }\n        OsFunction::Unlink => {\n            // args[0] is path\n            if get_virtual_file(&path).is_some() {\n                MUTABLE_VFS.with(|vfs| {\n                    let mut vfs = vfs.borrow_mut();\n                    vfs.files.remove(&path);\n                    vfs.deleted_files.insert(path);\n                });\n                MontyObject::None.into()\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n        OsFunction::Rmdir => {\n            // args[0] is path\n            if is_virtual_dir(&path) {\n                MUTABLE_VFS.with(|vfs| {\n                    let mut vfs = vfs.borrow_mut();\n                    vfs.dirs.remove(&path);\n                    vfs.deleted_dirs.insert(path);\n                });\n                MontyObject::None.into()\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n        OsFunction::Rename => {\n            // args[0] is src path, args[1] is dest path\n            let dest = match &args[1] {\n                MontyObject::Path(p) => p.clone(),\n                MontyObject::String(s) => s.clone(),\n                other => panic!(\"rename: second arg must be path, got {other:?}\"),\n            };\n\n            if let Some(file) = get_virtual_file(&path) {\n                MUTABLE_VFS.with(|vfs| {\n                    let mut vfs = vfs.borrow_mut();\n                    // Remove from old location\n                    vfs.files.remove(&path);\n                    vfs.deleted_files.insert(path);\n                    // Add to new location\n                    vfs.files.insert(dest, (file.content, file.mode));\n                });\n                MontyObject::None.into()\n            } else if is_virtual_dir(&path) {\n                MUTABLE_VFS.with(|vfs| {\n                    let mut vfs = vfs.borrow_mut();\n                    vfs.dirs.remove(&path);\n                    vfs.deleted_dirs.insert(path);\n                    vfs.dirs.insert(dest);\n                });\n                MontyObject::None.into()\n            } else {\n                MontyException::new(\n                    ExcType::FileNotFoundError,\n                    Some(format!(\"[Errno 2] No such file or directory: '{path}'\")),\n                )\n                .into()\n            }\n        }\n    }\n}\n\n/// Helper to create parent directories recursively.\nfn create_parent_dirs(path: &str) {\n    if is_virtual_dir(path) {\n        return;\n    }\n    // Create parent first\n    if let Some(parent) = std::path::Path::new(path).parent() {\n        let parent_str = parent.to_string_lossy().to_string();\n        if !parent_str.is_empty() {\n            create_parent_dirs(&parent_str);\n        }\n    }\n    // Create this directory\n    MUTABLE_VFS.with(|vfs| {\n        let mut vfs = vfs.borrow_mut();\n        vfs.dirs.insert(path.to_owned());\n    });\n}\n\n/// Represents a test failure with details about expected vs actual values.\n#[derive(Debug)]\nstruct TestFailure {\n    test_name: String,\n    kind: String,\n    expected: String,\n    actual: String,\n}\n\nimpl std::fmt::Display for TestFailure {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        writeln!(\n            f,\n            \"[{}] {} mismatch\\ngot {:?}\\ndiff:\",\n            self.test_name, self.kind, self.actual\n        )?;\n\n        for change in TextDiff::from_lines(&self.expected, &self.actual).iter_all_changes() {\n            write!(f, \"{}{}\", change.tag(), change)?;\n        }\n        Ok(())\n    }\n}\n\n/// Try to run a test, returning Ok(()) on success or Err with failure details.\n///\n/// This function executes Python code via the MontyRun and validates the result\n/// against the expected outcome specified in the fixture.\nfn try_run_test(path: &Path, code: &str, expectation: &Expectation) -> Result<(), TestFailure> {\n    let test_name = path.strip_prefix(\"test_cases/\").unwrap_or(path).display().to_string();\n\n    // Reset the mutable VFS for each test\n    reset_mutable_vfs();\n\n    // Handle ref-count-return tests separately since they need run_ref_counts()\n    #[cfg(feature = \"ref-count-return\")]\n    if let Expectation::RefCounts(expected) = expectation {\n        match MontyRun::new(code.to_owned(), &test_name, vec![]) {\n            Ok(ex) => {\n                let result = ex.run_ref_counts(vec![]);\n                match result {\n                    Ok(monty::RefCountOutput {\n                        counts,\n                        unique_refs,\n                        heap_count,\n                        ..\n                    }) => {\n                        // Strict matching: verify all heap objects are accounted for by variables\n                        if unique_refs != heap_count {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"Strict matching\".to_string(),\n                                expected: format!(\"{heap_count} heap objects\"),\n                                actual: format!(\"{unique_refs} referenced by variables, counts: {counts:?}\"),\n                            });\n                        }\n                        if &counts != expected {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"ref-counts\".to_string(),\n                                expected: format!(\"{expected:?}\"),\n                                actual: format!(\"{counts:?}\"),\n                            });\n                        }\n                        return Ok(());\n                    }\n                    Err(e) => {\n                        return Err(TestFailure {\n                            test_name,\n                            kind: \"Runtime\".to_string(),\n                            expected: \"success\".to_string(),\n                            actual: e.to_string(),\n                        });\n                    }\n                }\n            }\n            Err(parse_err) => {\n                return Err(TestFailure {\n                    test_name,\n                    kind: \"Parse\".to_string(),\n                    expected: \"success\".to_string(),\n                    actual: parse_err.to_string(),\n                });\n            }\n        }\n    }\n\n    match MontyRun::new(code.to_owned(), &test_name, vec![]) {\n        Ok(ex) => {\n            let limits = ResourceLimits::new().max_recursion_depth(Some(TEST_RECURSION_LIMIT));\n            let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n            match result {\n                Ok(obj) => match expectation {\n                    Expectation::ReturnStr(expected) => {\n                        let output = obj.to_string();\n                        if output != *expected {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"str()\".to_string(),\n                                expected: expected.clone(),\n                                actual: output,\n                            });\n                        }\n                    }\n                    Expectation::Return(expected) => {\n                        let output = obj.py_repr();\n                        if output != *expected {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"py_repr()\".to_string(),\n                                expected: expected.clone(),\n                                actual: output,\n                            });\n                        }\n                    }\n                    Expectation::ReturnType(expected) => {\n                        let output = obj.type_name();\n                        if output != expected {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"type_name()\".to_string(),\n                                expected: expected.clone(),\n                                actual: output.to_string(),\n                            });\n                        }\n                    }\n                    #[cfg(not(feature = \"ref-count-return\"))]\n                    Expectation::RefCounts(_) => {\n                        // Skip ref-count tests when feature is disabled\n                    }\n                    Expectation::NoException => {\n                        // Success - code ran without exception as expected\n                    }\n                    Expectation::Raise(expected) | Expectation::Traceback(expected) => {\n                        return Err(TestFailure {\n                            test_name,\n                            kind: \"Exception\".to_string(),\n                            expected: expected.clone(),\n                            actual: \"no exception raised\".to_string(),\n                        });\n                    }\n                    #[cfg(feature = \"ref-count-return\")]\n                    Expectation::RefCounts(_) => unreachable!(),\n                },\n                Err(e) => {\n                    if let Expectation::Raise(expected) = expectation {\n                        let output = e.py_repr();\n                        if output != *expected {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"Exception\".to_string(),\n                                expected: expected.clone(),\n                                actual: output,\n                            });\n                        }\n                    } else if let Expectation::Traceback(expected) = expectation {\n                        let output = e.to_string();\n                        if output != *expected {\n                            return Err(TestFailure {\n                                test_name,\n                                kind: \"Traceback\".to_string(),\n                                expected: expected.clone(),\n                                actual: output,\n                            });\n                        }\n                    } else {\n                        return Err(TestFailure {\n                            test_name,\n                            kind: \"Unexpected error\".to_string(),\n                            expected: \"success\".to_string(),\n                            actual: e.to_string(),\n                        });\n                    }\n                }\n            }\n        }\n        Err(parse_err) => {\n            if let Expectation::Raise(expected) = expectation {\n                let output = parse_err.py_repr();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"Parse error\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n            } else if let Expectation::Traceback(expected) = expectation {\n                let output = parse_err.to_string();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"Traceback\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n            } else {\n                return Err(TestFailure {\n                    test_name,\n                    kind: \"Unexpected parse error\".to_string(),\n                    expected: \"success\".to_string(),\n                    actual: parse_err.to_string(),\n                });\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Try to run a test using MontyRun with external function support.\n///\n/// This function handles tests marked with `# call-external` directive by using the\n/// iterative executor API and providing implementations for predefined external functions.\nfn try_run_iter_test(path: &Path, code: &str, expectation: &Expectation) -> Result<(), TestFailure> {\n    let test_name = path.strip_prefix(\"test_cases/\").unwrap_or(path).display().to_string();\n\n    // Reset the mutable VFS for each test\n    reset_mutable_vfs();\n\n    // Ref-counting tests not supported in iter mode\n    #[cfg(feature = \"ref-count-return\")]\n    if matches!(expectation, Expectation::RefCounts(_)) {\n        return Err(TestFailure {\n            test_name,\n            kind: \"Configuration\".to_string(),\n            expected: \"non-refcount test\".to_string(),\n            actual: \"ref-counts tests are not supported in iter mode\".to_string(),\n        });\n    }\n\n    let exec = match MontyRun::new(code.to_owned(), &test_name, vec![]) {\n        Ok(e) => e,\n        Err(parse_err) => {\n            if let Expectation::Raise(expected) = expectation {\n                let output = parse_err.py_repr();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"Parse error\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n                return Ok(());\n            } else if let Expectation::Traceback(expected) = expectation {\n                let output = parse_err.to_string();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"Traceback\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n                return Ok(());\n            }\n            return Err(TestFailure {\n                test_name,\n                kind: \"Unexpected parse error\".to_string(),\n                expected: \"success\".to_string(),\n                actual: parse_err.to_string(),\n            });\n        }\n    };\n\n    // Run execution loop, handling external function calls until complete\n    let result = run_iter_loop(exec);\n\n    match result {\n        Ok(obj) => match expectation {\n            Expectation::ReturnStr(expected) => {\n                let output = obj.to_string();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"str()\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n            }\n            Expectation::Return(expected) => {\n                let output = obj.py_repr();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"py_repr()\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n            }\n            Expectation::ReturnType(expected) => {\n                let output = obj.type_name();\n                if output != expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"type_name()\".to_string(),\n                        expected: expected.clone(),\n                        actual: output.to_string(),\n                    });\n                }\n            }\n            #[cfg(not(feature = \"ref-count-return\"))]\n            Expectation::RefCounts(_) => {}\n            Expectation::NoException => {}\n            Expectation::Raise(expected) | Expectation::Traceback(expected) => {\n                return Err(TestFailure {\n                    test_name,\n                    kind: \"Exception\".to_string(),\n                    expected: expected.clone(),\n                    actual: \"no exception raised\".to_string(),\n                });\n            }\n            #[cfg(feature = \"ref-count-return\")]\n            Expectation::RefCounts(_) => unreachable!(),\n        },\n        Err(e) => {\n            if let Expectation::Raise(expected) = expectation {\n                let output = e.py_repr();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"Exception\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n            } else if let Expectation::Traceback(expected) = expectation {\n                let output = e.to_string();\n                if output != *expected {\n                    return Err(TestFailure {\n                        test_name,\n                        kind: \"Traceback\".to_string(),\n                        expected: expected.clone(),\n                        actual: output,\n                    });\n                }\n            } else {\n                return Err(TestFailure {\n                    test_name,\n                    kind: \"Unexpected error\".to_string(),\n                    expected: \"success\".to_string(),\n                    actual: e.to_string(),\n                });\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Execute the iter loop, dispatching external function calls until complete.\n///\n/// When `ref-count-panic` feature is NOT enabled, this function also tests\n/// serialization round-trips by dumping and loading the execution state at\n/// each external function call boundary.\n///\n/// Supports both synchronous and asynchronous external functions:\n/// - Sync functions: result is passed immediately via `state.run()`\n/// - Async functions: `state.run_pending()` creates a future, resolved via `ResolveFutures`\nfn run_iter_loop(exec: MontyRun) -> Result<MontyObject, MontyException> {\n    let limits = ResourceLimits::new().max_recursion_depth(Some(TEST_RECURSION_LIMIT));\n    let mut progress = exec.start(vec![], LimitedTracker::new(limits), PrintWriter::Stdout)?;\n\n    // Track pending async calls: (call_id, result_value)\n    let mut pending_results: Vec<(u32, MontyObject)> = Vec::new();\n\n    loop {\n        // Test serialization round-trip at each step (skip when ref-count-panic is enabled\n        // since the old RunProgress would panic on drop without proper cleanup)\n        #[cfg(not(feature = \"ref-count-panic\"))]\n        {\n            let bytes = progress.dump().expect(\"failed to dump RunProgress\");\n            progress = RunProgress::load(&bytes).expect(\"failed to load RunProgress\");\n        }\n\n        match progress {\n            RunProgress::Complete(result) => return Ok(result),\n            RunProgress::FunctionCall(call) => {\n                // Method calls on dataclasses are dispatched to the host.\n                // Dispatch known methods; return AttributeError for unknown ones.\n                if call.method_call {\n                    let result = dispatch_method_call(&call.function_name, &call.args, &call.kwargs);\n                    progress = call.resume(result, PrintWriter::Stdout)?;\n                    continue;\n                }\n                let dispatch_result = dispatch_external_call(&call.function_name, call.args.clone());\n                match dispatch_result {\n                    DispatchResult::Sync(return_value) => {\n                        progress = call.resume(return_value, PrintWriter::Stdout)?;\n                    }\n                    DispatchResult::Async(result_value) => {\n                        // Store the result for later resolution\n                        pending_results.push((call.call_id, result_value));\n                        // Continue execution with a pending future\n                        progress = call.resume_pending(PrintWriter::Stdout)?;\n                    }\n                }\n            }\n            RunProgress::ResolveFutures(state) => {\n                // Resolve all pending futures that we have results for\n                let results: Vec<(u32, ExtFunctionResult)> = state\n                    .pending_call_ids()\n                    .iter()\n                    .filter_map(|p| {\n                        pending_results.iter().position(|(id, _)| id == p).map(|idx| {\n                            let (call_id, value) = pending_results.remove(idx);\n                            (call_id, ExtFunctionResult::Return(value))\n                        })\n                    })\n                    .collect();\n\n                assert!(\n                    !results.is_empty(),\n                    \"ResolveFutures: no results available for pending calls: {:?}\",\n                    state.pending_call_ids().iter().collect::<Vec<_>>()\n                );\n\n                progress = state.resume(results, PrintWriter::Stdout)?;\n            }\n            RunProgress::NameLookup(lookup) => {\n                let result = match lookup.name.as_str() {\n                    // External functions — resolved as callable Function objects\n                    \"add_ints\" | \"concat_strings\" | \"return_value\" | \"get_list\" | \"raise_error\" | \"make_point\"\n                    | \"make_mutable_point\" | \"make_user\" | \"make_empty\" | \"async_call\" => {\n                        NameLookupResult::Value(MontyObject::Function {\n                            name: lookup.name.clone(),\n                            docstring: None,\n                        })\n                    }\n                    // Non-function constants — resolved as plain values\n                    \"CONST_INT\" => NameLookupResult::Value(MontyObject::Int(42)),\n                    \"CONST_STR\" => NameLookupResult::Value(MontyObject::String(\"hello\".to_string())),\n                    #[expect(clippy::approx_constant, reason = \"3.14 is the intended test value\")]\n                    \"CONST_FLOAT\" => NameLookupResult::Value(MontyObject::Float(3.14)),\n                    \"CONST_BOOL\" => NameLookupResult::Value(MontyObject::Bool(true)),\n                    \"CONST_LIST\" => NameLookupResult::Value(MontyObject::List(vec![\n                        MontyObject::Int(1),\n                        MontyObject::Int(2),\n                        MontyObject::Int(3),\n                    ])),\n                    \"CONST_NONE\" => NameLookupResult::Value(MontyObject::None),\n                    // Unknown names → NameError\n                    _ => NameLookupResult::Undefined,\n                };\n                progress = lookup.resume(result, PrintWriter::Stdout)?;\n            }\n            RunProgress::OsCall(call) => {\n                let result = dispatch_os_call(call.function, &call.args, &call.kwargs);\n                progress = call.resume(result, PrintWriter::Stdout)?;\n            }\n        }\n    }\n}\n\n/// Split Python code into statements and a final expression to evaluate.\n///\n/// For Return expectations, the last non-empty line is the expression to evaluate.\n/// For Raise/NoException, the entire code is statements (returns None for expression).\n///\n/// Returns (statements_code, optional_final_expression).\nfn split_code_for_module(code: &str, need_return_value: bool) -> (String, Option<String>) {\n    let lines: Vec<&str> = code.lines().collect();\n\n    // Find the last non-empty line\n    let last_idx = lines\n        .iter()\n        .rposition(|line| !line.trim().is_empty())\n        .expect(\"Empty code\");\n\n    if need_return_value {\n        let last_line = lines[last_idx].trim();\n\n        // Check if the last line is a statement (can't be evaluated as an expression)\n        // Matches both `assert expr` and `assert(expr)` forms\n        if last_line.starts_with(\"assert \") || last_line.starts_with(\"assert(\") {\n            // All code is statements, no expression to evaluate\n            (lines[..=last_idx].join(\"\\n\"), None)\n        } else {\n            // Everything except last line is statements, last line is the expression\n            let statements = lines[..last_idx].join(\"\\n\");\n            let expr = last_line.to_string();\n            (statements, Some(expr))\n        }\n    } else {\n        // All code is statements (for exception tests or NoException)\n        (lines[..=last_idx].join(\"\\n\"), None)\n    }\n}\n\n/// Wraps code in an async context for CPython execution.\n///\n/// Monty supports top-level `await`, but CPython does not. This function transforms code\n/// like:\n///\n/// ```python\n/// async def foo():\n///     return 1\n/// result = await foo()\n/// ```\n///\n/// Into:\n///\n/// ```python\n/// import asyncio\n/// async def __test_main():\n///     async def foo():\n///         return 1\n///     result = await foo()\n///     return result  # if need_return_value\n/// __test_result__ = asyncio.run(__test_main())\n/// ```\nfn wrap_code_for_async(code: &str, need_return_value: bool) -> (String, Option<String>) {\n    let lines: Vec<&str> = code.lines().collect();\n\n    // Find the last non-empty, non-comment line\n    let last_idx = lines\n        .iter()\n        .rposition(|line| {\n            let trimmed = line.trim();\n            !trimmed.is_empty() && !trimmed.starts_with('#')\n        })\n        .expect(\"Empty code\");\n\n    // Indent all code by 4 spaces for the function body\n    let indented: String = lines\n        .iter()\n        .map(|line| {\n            if line.is_empty() {\n                String::new()\n            } else {\n                format!(\"    {line}\")\n            }\n        })\n        .collect::<Vec<_>>()\n        .join(\"\\n\");\n\n    let return_stmt = if need_return_value {\n        // The last non-empty, non-comment line is the expression to return\n        let last_line = lines[last_idx].trim();\n        format!(\"\\n    return {last_line}\")\n    } else {\n        String::new()\n    };\n\n    let wrapped = format!(\n        \"import asyncio\\nasync def __test_main():\\n{indented}{return_stmt}\\n__test_result__ = asyncio.run(__test_main())\"\n    );\n\n    if need_return_value {\n        (wrapped, Some(\"__test_result__\".to_string()))\n    } else {\n        (wrapped, None)\n    }\n}\n\n/// Run the traceback script to get CPython's traceback output for a test file.\n///\n/// This imports scripts/run_traceback.py via pyo3 and calls `run_file_and_get_traceback()`\n/// which executes the file via runpy.run_path() to ensure full traceback information\n/// (including caret lines) is preserved.\n///\n/// When `iter_mode` is true, external function implementations are injected into the\n/// file's globals before execution.\n///\n/// When `async_mode` is true, code is wrapped in an async context before execution.\nfn run_traceback_script(path: &Path, iter_mode: bool, async_mode: bool) -> String {\n    Python::attach(|py| {\n        let run_traceback = import_run_traceback(py);\n\n        // Get absolute path for the test file\n        let abs_path = path.canonicalize().expect(\"Failed to get absolute path\");\n        let path_str = abs_path.to_str().expect(\"Invalid UTF-8 in path\");\n\n        // Call run_file_and_get_traceback with the recursion limit, iter_mode, and async_mode flags\n        let result = run_traceback\n            .call_method1(\n                \"run_file_and_get_traceback\",\n                (path_str, TEST_RECURSION_LIMIT, iter_mode, async_mode),\n            )\n            .expect(\"Failed to call run_file_and_get_traceback\");\n\n        // Handle None return (no exception raised)\n        if result.is_none() {\n            String::new()\n        } else {\n            result\n                .extract()\n                .expect(\"Failed to extract string from return value of run_file_and_get_traceback\")\n        }\n    })\n}\n\nfn format_traceback(py: Python<'_>, exc: &PyErr) -> String {\n    let run_traceback = import_run_traceback(py);\n    let exc_value = exc.value(py);\n    let return_value = run_traceback\n        .call_method1(\"format_full_traceback\", (exc_value,))\n        .expect(\"Failed to call format_full_traceback\");\n    return_value\n        .extract()\n        .expect(\"failed to extract string from return value of format_full_traceback\")\n}\n\n/// Import the run_traceback module\nfn import_run_traceback(py: Python<'_>) -> Bound<'_, PyModule> {\n    // Add scripts directory to sys.path (tests run from crates/monty/)\n    let sys = py.import(\"sys\").expect(\"Failed to import sys\");\n    let sys_path = sys.getattr(\"path\").expect(\"Failed to get sys.path\");\n    sys_path\n        .call_method1(\"insert\", (0, \"../../scripts\"))\n        .expect(\"Failed to add scripts to sys.path\");\n\n    // Import the run_traceback module\n    py.import(\"run_traceback\").expect(\"Failed to import run_traceback\")\n}\n\n/// Result from CPython execution - either a value to compare, or an early return.\nenum CpythonResult {\n    /// Value to compare against expectation\n    Value(String),\n    /// No value to compare (NoException test succeeded)\n    NoValue,\n    /// Test failed with this error\n    Failed(TestFailure),\n}\n\n/// Try to run a test through CPython, returning Ok(()) on success or Err with failure details.\n///\n/// This function executes the same Python code via CPython (using pyo3) and\n/// compares the result with the expected value. This ensures Monty behaves\n/// identically to CPython.\n///\n/// Code is executed at module level (not wrapped in a function) so that\n/// `global` keyword semantics work correctly.\n///\n/// RefCounts tests are skipped as they're Monty-specific.\n/// Traceback tests use scripts/run_traceback.py for reliable caret line support.\nfn try_run_cpython_test(\n    path: &Path,\n    code: &str,\n    expectation: &Expectation,\n    iter_mode: bool,\n    async_mode: bool,\n) -> Result<(), TestFailure> {\n    // Ensure Python modules are imported before parallel tests access them.\n    // This prevents race conditions during module initialization.\n    ensure_python_modules_imported();\n\n    // Skip RefCounts tests - only relevant for Monty\n    if matches!(expectation, Expectation::RefCounts(_)) {\n        return Ok(());\n    }\n\n    let test_name = path.strip_prefix(\"test_cases/\").unwrap_or(path).display().to_string();\n\n    // Traceback tests use the external script for reliable caret line support\n    if let Expectation::Traceback(expected) = expectation {\n        let result = run_traceback_script(path, iter_mode, async_mode);\n        if result != *expected {\n            return Err(TestFailure {\n                test_name,\n                kind: \"CPython traceback\".to_string(),\n                expected: expected.clone(),\n                actual: result,\n            });\n        }\n        return Ok(());\n    }\n\n    let need_return_value = matches!(\n        expectation,\n        Expectation::Return(_) | Expectation::ReturnStr(_) | Expectation::ReturnType(_)\n    );\n\n    // Use async wrapper for tests with top-level await\n    let (statements, maybe_expr) = if async_mode {\n        wrap_code_for_async(code, need_return_value)\n    } else {\n        split_code_for_module(code, need_return_value)\n    };\n\n    let result: CpythonResult = Python::attach(|py| {\n        // Execute statements at module level\n        let globals = PyDict::new(py);\n\n        // For iter mode tests, inject external function implementations into globals\n        if iter_mode {\n            let ext_funcs_cstr = CString::new(ITER_EXT_FUNCTIONS_PYTHON).expect(\"Invalid C string in ext funcs\");\n            py.run(&ext_funcs_cstr, Some(&globals), None)\n                .expect(\"Failed to define external functions for iter mode\");\n        }\n\n        // Run the statements\n        let statements_cstr = CString::new(statements.as_str()).expect(\"Invalid C string in statements\");\n        let stmt_result = py.run(&statements_cstr, Some(&globals), None);\n\n        // Handle exception during statement execution\n        if let Err(e) = stmt_result {\n            if matches!(expectation, Expectation::NoException) {\n                return CpythonResult::Failed(TestFailure {\n                    test_name: test_name.clone(),\n                    kind: \"CPython unexpected exception\".to_string(),\n                    expected: \"no exception\".to_string(),\n                    actual: format_traceback(py, &e),\n                });\n            }\n            if matches!(expectation, Expectation::Raise(_)) {\n                return CpythonResult::Value(format_cpython_exception(py, &e));\n            }\n            return CpythonResult::Failed(TestFailure {\n                test_name: test_name.clone(),\n                kind: \"CPython unexpected exception\".to_string(),\n                expected: \"success\".to_string(),\n                actual: format_traceback(py, &e),\n            });\n        }\n\n        // If we have an expression to evaluate, evaluate it\n        if let Some(expr) = maybe_expr {\n            let expr_cstr = CString::new(expr.as_str()).expect(\"Invalid C string in expr\");\n            match py.eval(&expr_cstr, Some(&globals), None) {\n                Ok(result) => {\n                    // Code returned successfully - format based on expectation type\n                    match expectation {\n                        Expectation::Return(_) => CpythonResult::Value(result.repr().unwrap().to_string()),\n                        Expectation::ReturnStr(_) => CpythonResult::Value(result.str().unwrap().to_string()),\n                        Expectation::ReturnType(_) => {\n                            CpythonResult::Value(result.get_type().name().unwrap().to_string())\n                        }\n                        Expectation::Raise(expected) => CpythonResult::Failed(TestFailure {\n                            test_name: test_name.clone(),\n                            kind: \"CPython exception\".to_string(),\n                            expected: expected.clone(),\n                            actual: \"no exception raised\".to_string(),\n                        }),\n                        // Traceback tests are handled by run_traceback_script above\n                        Expectation::Traceback(_) | Expectation::NoException | Expectation::RefCounts(_) => {\n                            unreachable!()\n                        }\n                    }\n                }\n                Err(e) => {\n                    // Expression raised an exception\n                    if matches!(expectation, Expectation::NoException) {\n                        return CpythonResult::Failed(TestFailure {\n                            test_name: test_name.clone(),\n                            kind: \"CPython unexpected exception\".to_string(),\n                            expected: \"no exception\".to_string(),\n                            actual: format_traceback(py, &e),\n                        });\n                    }\n                    if matches!(expectation, Expectation::Raise(_)) {\n                        return CpythonResult::Value(format_cpython_exception(py, &e));\n                    }\n                    // Traceback tests are handled by run_traceback_script above\n                    CpythonResult::Failed(TestFailure {\n                        test_name: test_name.clone(),\n                        kind: \"CPython unexpected exception\".to_string(),\n                        expected: \"success\".to_string(),\n                        actual: format_traceback(py, &e),\n                    })\n                }\n            }\n        } else {\n            // No expression to evaluate\n            // Traceback tests are handled by run_traceback_script above\n            if let Expectation::Raise(expected) = expectation {\n                return CpythonResult::Failed(TestFailure {\n                    test_name: test_name.clone(),\n                    kind: \"CPython exception\".to_string(),\n                    expected: expected.clone(),\n                    actual: \"no exception raised\".to_string(),\n                });\n            }\n            CpythonResult::NoValue // NoException expectation - success\n        }\n    });\n\n    match result {\n        CpythonResult::Value(actual) => {\n            let expected = expectation.expected_value();\n            if actual != expected {\n                return Err(TestFailure {\n                    test_name,\n                    kind: \"CPython result\".to_string(),\n                    expected: expected.to_string(),\n                    actual,\n                });\n            }\n            Ok(())\n        }\n        CpythonResult::NoValue => Ok(()),\n        CpythonResult::Failed(failure) => Err(failure),\n    }\n}\n\n/// Format a CPython exception into the expected format.\nfn format_cpython_exception(py: Python<'_>, e: &PyErr) -> String {\n    let exc_type = e.get_type(py).name().unwrap();\n    let exc_message: String = e\n        .value(py)\n        .getattr(\"args\")\n        .and_then(|args| args.get_item(0))\n        .and_then(|item| item.extract())\n        .unwrap_or_default();\n\n    if exc_message.is_empty() {\n        format!(\"{exc_type}()\")\n    } else if exc_message.contains('\\'') {\n        // Use double quotes when message contains single quotes (like Python's repr)\n        format!(\"{exc_type}(\\\"{exc_message}\\\")\")\n    } else {\n        // Use single quotes (default Python repr format)\n        format!(\"{exc_type}('{exc_message}')\")\n    }\n}\n\n/// Timeout duration for Monty tests.\n///\n/// Tests that exceed this duration are considered to be hanging (infinite loop)\n/// and will fail with a timeout error.\nconst TEST_TIMEOUT: Duration = Duration::from_secs(2);\n\n/// Result from running a test with a timeout.\nenum TimeoutResult<T> {\n    /// The closure completed successfully.\n    Ok(T),\n    /// The closure panicked with the given message.\n    Panicked(String),\n    /// The timeout was exceeded.\n    TimedOut,\n}\n\n/// Runs a closure with a timeout, returning an error if it exceeds the duration or panics.\n///\n/// Spawns the closure in a separate thread and waits for the result with a timeout.\n/// Distinguishes between three cases:\n/// - Success: the closure returned normally\n/// - Panic: the closure panicked (detected via channel disconnect + catch_unwind)\n/// - Timeout: the timeout was exceeded (possible infinite loop)\n///\n/// Note that if a timeout occurs, the spawned thread will continue running in the\n/// background (Rust doesn't support killing threads), but the test will fail immediately.\nfn run_with_timeout<F, T>(timeout: Duration, f: F) -> TimeoutResult<T>\nwhere\n    F: FnOnce() -> T + Send + 'static,\n    T: Send + 'static,\n{\n    let (tx, rx) = mpsc::channel();\n    thread::spawn(move || {\n        // Catch panics so we can report them properly instead of as timeouts\n        let result = panic::catch_unwind(AssertUnwindSafe(f));\n        match result {\n            Ok(value) => {\n                let _ = tx.send(Ok(value));\n            }\n            Err(panic_payload) => {\n                // Extract panic message from the payload\n                let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {\n                    (*s).to_string()\n                } else if let Some(s) = panic_payload.downcast_ref::<String>() {\n                    s.clone()\n                } else {\n                    \"unknown panic\".to_string()\n                };\n                let _ = tx.send(Err(msg));\n            }\n        }\n    });\n\n    match rx.recv_timeout(timeout) {\n        Ok(Ok(value)) => TimeoutResult::Ok(value),\n        Ok(Err(panic_msg)) => TimeoutResult::Panicked(panic_msg),\n        Err(RecvTimeoutError::Timeout) => TimeoutResult::TimedOut,\n        // Disconnected without sending means something went very wrong\n        Err(RecvTimeoutError::Disconnected) => {\n            TimeoutResult::Panicked(\"thread terminated without sending result\".to_string())\n        }\n    }\n}\n\n/// Test function that runs each fixture through Monty.\n///\n/// Handles xfail with strict semantics: if a test is marked `xfail=monty`, it must fail.\n/// If an xfail test passes unexpectedly, that's an error.\nfn run_test_cases_monty(path: &Path) -> Result<(), Box<dyn Error>> {\n    let content = fs::read_to_string(path)?;\n    let (code, expectation, config) = parse_fixture(&content);\n    let test_name = path.strip_prefix(\"test_cases/\").unwrap_or(path).display().to_string();\n\n    // Move data into the closure since it needs 'static lifetime\n    let path_owned = path.to_owned();\n    let iter_mode = config.iter_mode;\n\n    let result = run_with_timeout(TEST_TIMEOUT, move || {\n        if iter_mode {\n            try_run_iter_test(&path_owned, &code, &expectation)\n        } else {\n            try_run_test(&path_owned, &code, &expectation)\n        }\n    });\n\n    // Handle timeout/panic errors from the test thread\n    let result = match result {\n        TimeoutResult::Ok(inner_result) => inner_result,\n        TimeoutResult::Panicked(panic_msg) => Err(TestFailure {\n            test_name: test_name.clone(),\n            kind: \"Panic\".to_string(),\n            expected: \"no panic\".to_string(),\n            actual: format!(\"test panicked: {panic_msg}\"),\n        }),\n        TimeoutResult::TimedOut => Err(TestFailure {\n            test_name: test_name.clone(),\n            kind: \"Timeout\".to_string(),\n            expected: format!(\"completion within {TEST_TIMEOUT:?}\"),\n            actual: format!(\"test timed out after {TEST_TIMEOUT:?} (possible infinite loop)\"),\n        }),\n    };\n\n    if config.xfail_monty {\n        // Strict xfail: test must fail; if it passed, xfail should be removed\n        assert!(\n            result.is_err(),\n            \"[{test_name}] Test marked xfail=monty passed unexpectedly. Remove xfail if the test is now fixed.\"\n        );\n    } else if let Err(failure) = result {\n        panic!(\"{failure}\");\n    }\n    Ok(())\n}\n\n/// Test function that runs each fixture through CPython.\n///\n/// Handles xfail with strict semantics: if a test is marked `xfail=cpython`, it must fail.\n/// If an xfail test passes unexpectedly, that's an error.\nfn run_test_cases_cpython(path: &Path) -> Result<(), Box<dyn Error>> {\n    let content = fs::read_to_string(path)?;\n    let (code, expectation, config) = parse_fixture(&content);\n    let test_name = path.strip_prefix(\"test_cases/\").unwrap_or(path).display().to_string();\n\n    let result = try_run_cpython_test(path, &code, &expectation, config.iter_mode, config.async_mode);\n\n    if config.xfail_cpython {\n        // Strict xfail: test must fail; if it passed, xfail should be removed\n        assert!(\n            result.is_err(),\n            \"[{test_name}] Test marked xfail=cpython passed unexpectedly. Remove xfail if the test is now fixed.\"\n        );\n    } else if let Err(failure) = result {\n        panic!(\"{failure}\");\n    }\n    Ok(())\n}\n\n// Generate tests for all fixture files using datatest-stable harness macro\ndatatest_stable::harness!(\n    run_test_cases_monty,\n    \"test_cases\",\n    r\"^.*\\.py$\",\n    run_test_cases_cpython,\n    \"test_cases\",\n    r\"^.*\\.py$\",\n);\n"
  },
  {
    "path": "crates/monty/tests/inputs.rs",
    "content": "//! Tests for passing input values to the executor.\n//!\n//! These tests verify that `MontyObject` inputs are correctly converted to `Object`\n//! and can be used in Python code execution.\n\nuse indexmap::IndexMap;\nuse monty::{ExcType, MontyObject, MontyRun};\n\n// === Immediate Value Tests ===\n\n#[test]\nfn input_int() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Int(42)]).unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n#[test]\nfn input_int_arithmetic() {\n    let ex = MontyRun::new(\"x + 1\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Int(41)]).unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n#[test]\nfn input_bool_true() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Bool(true)]).unwrap();\n    assert_eq!(result, MontyObject::Bool(true));\n}\n\n#[test]\nfn input_bool_false() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Bool(false)]).unwrap();\n    assert_eq!(result, MontyObject::Bool(false));\n}\n\n#[test]\nfn input_float() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Float(2.5)]).unwrap();\n    assert_eq!(result, MontyObject::Float(2.5));\n}\n\n#[test]\nfn input_none() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::None]).unwrap();\n    assert_eq!(result, MontyObject::None);\n}\n\n#[test]\nfn input_ellipsis() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Ellipsis]).unwrap();\n    assert_eq!(result, MontyObject::Ellipsis);\n}\n\n// === Heap-Allocated Value Tests ===\n\n#[test]\nfn input_string() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::String(\"hello\".to_string())])\n        .unwrap();\n    assert_eq!(result, MontyObject::String(\"hello\".to_string()));\n}\n\n#[test]\nfn input_string_concat() {\n    let ex = MontyRun::new(\"x + ' world'\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::String(\"hello\".to_string())])\n        .unwrap();\n    assert_eq!(result, MontyObject::String(\"hello world\".to_string()));\n}\n\n#[test]\nfn input_bytes() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Bytes(vec![1, 2, 3])]).unwrap();\n    assert_eq!(result, MontyObject::Bytes(vec![1, 2, 3]));\n}\n\n#[test]\nfn input_list() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])\n    );\n}\n\n#[test]\nfn input_list_append() {\n    let ex = MontyRun::new(\"x.append(3)\\nx\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2), MontyObject::Int(3)])\n    );\n}\n\n#[test]\nfn input_tuple() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::Tuple(vec![\n            MontyObject::Int(1),\n            MontyObject::String(\"two\".to_string()),\n        ])])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::Tuple(vec![MontyObject::Int(1), MontyObject::String(\"two\".to_string())])\n    );\n}\n\n#[test]\nfn input_dict() {\n    let mut map = IndexMap::new();\n    map.insert(MontyObject::String(\"a\".to_string()), MontyObject::Int(1));\n\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::dict(map)]).unwrap();\n\n    // Build expected map for comparison\n    let mut expected = IndexMap::new();\n    expected.insert(MontyObject::String(\"a\".to_string()), MontyObject::Int(1));\n    assert_eq!(result, MontyObject::Dict(expected.into()));\n}\n\n#[test]\nfn input_dict_get() {\n    let mut map = IndexMap::new();\n    map.insert(MontyObject::String(\"key\".to_string()), MontyObject::Int(42));\n\n    let ex = MontyRun::new(\"x['key']\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::dict(map)]).unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n// === Multiple Inputs ===\n\n#[test]\nfn multiple_inputs_two() {\n    let ex = MontyRun::new(\"x + y\".to_owned(), \"test.py\", vec![\"x\".to_owned(), \"y\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::Int(10), MontyObject::Int(32)])\n        .unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n#[test]\nfn multiple_inputs_three() {\n    let ex = MontyRun::new(\n        \"x + y + z\".to_owned(),\n        \"test.py\",\n        vec![\"x\".to_owned(), \"y\".to_owned(), \"z\".to_owned()],\n    )\n    .unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::Int(10), MontyObject::Int(20), MontyObject::Int(12)])\n        .unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n#[test]\nfn multiple_inputs_mixed_types() {\n    // Create a list from two inputs\n    let ex = MontyRun::new(\"[x, y]\".to_owned(), \"test.py\", vec![\"x\".to_owned(), \"y\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::Int(1), MontyObject::String(\"two\".to_string())])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::List(vec![MontyObject::Int(1), MontyObject::String(\"two\".to_string())])\n    );\n}\n\n// === Edge Cases ===\n\n#[test]\nfn no_inputs() {\n    let ex = MontyRun::new(\"42\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(result, MontyObject::Int(42));\n}\n\n#[test]\nfn nested_list() {\n    let ex = MontyRun::new(\"x[0][1]\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::List(vec![MontyObject::List(vec![\n            MontyObject::Int(1),\n            MontyObject::Int(2),\n        ])])])\n        .unwrap();\n    assert_eq!(result, MontyObject::Int(2));\n}\n\n#[test]\nfn empty_list_input() {\n    let ex = MontyRun::new(\"len(x)\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::List(vec![])]).unwrap();\n    assert_eq!(result, MontyObject::Int(0));\n}\n\n#[test]\nfn empty_string_input() {\n    let ex = MontyRun::new(\"len(x)\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::String(String::new())]).unwrap();\n    assert_eq!(result, MontyObject::Int(0));\n}\n\n// === Exception Input Tests ===\n\n#[test]\nfn input_exception() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::Exception {\n            exc_type: ExcType::ValueError,\n            arg: Some(\"test message\".to_string()),\n        }])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::Exception {\n            exc_type: ExcType::ValueError,\n            arg: Some(\"test message\".to_string()),\n        }\n    );\n}\n\n#[test]\nfn input_exception_no_arg() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::Exception {\n            exc_type: ExcType::TypeError,\n            arg: None,\n        }])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::Exception {\n            exc_type: ExcType::TypeError,\n            arg: None,\n        }\n    );\n}\n\n#[test]\nfn input_exception_in_list() {\n    let ex = MontyRun::new(\"x[0]\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex\n        .run_no_limits(vec![MontyObject::List(vec![MontyObject::Exception {\n            exc_type: ExcType::KeyError,\n            arg: Some(\"key\".to_string()),\n        }])])\n        .unwrap();\n    assert_eq!(\n        result,\n        MontyObject::Exception {\n            exc_type: ExcType::KeyError,\n            arg: Some(\"key\".to_string()),\n        }\n    );\n}\n\n#[test]\nfn input_exception_raise() {\n    // Test that an exception passed as input can be raised\n    let ex = MontyRun::new(\"raise x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Exception {\n        exc_type: ExcType::ValueError,\n        arg: Some(\"input error\".to_string()),\n    }]);\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::ValueError);\n    assert_eq!(exc.message(), Some(\"input error\"));\n}\n\n// === Invalid Input Tests ===\n\n#[test]\nfn invalid_input_repr() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    let result = ex.run_no_limits(vec![MontyObject::Repr(\"some repr\".to_string())]);\n    assert!(result.is_err(), \"Repr should not be a valid input\");\n}\n\n#[test]\nfn invalid_input_repr_nested_in_list() {\n    let ex = MontyRun::new(\"x\".to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    // Repr nested inside a list should still be invalid\n    let result = ex.run_no_limits(vec![MontyObject::List(vec![MontyObject::Repr(\n        \"nested repr\".to_string(),\n    )])]);\n    assert!(result.is_err(), \"Repr nested in list should be invalid\");\n}\n\n// === Function Parameter Shadowing Tests ===\n// These tests verify that function parameters properly shadow script inputs with the same name.\n\n#[test]\nfn function_param_shadows_input() {\n    // Function parameter `x` should shadow the script input `x`\n    let code = \"\ndef foo(x):\n    return x + 1\n\nfoo(x * 2)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    // x=5 (input), foo(x * 2) = foo(10), inside foo x=10 (param), returns 11\n    let result = ex.run_no_limits(vec![MontyObject::Int(5)]).unwrap();\n    assert_eq!(result, MontyObject::Int(11));\n}\n\n#[test]\nfn function_param_shadows_input_multiple_params() {\n    // Multiple function parameters should all shadow their corresponding inputs\n    let code = \"\ndef add(x, y):\n    return x + y\n\nadd(x * 10, y * 100)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned(), \"y\".to_owned()]).unwrap();\n    // x=2, y=3 (inputs), add(20, 300), inside add x=20, y=300, returns 320\n    let result = ex\n        .run_no_limits(vec![MontyObject::Int(2), MontyObject::Int(3)])\n        .unwrap();\n    assert_eq!(result, MontyObject::Int(320));\n}\n\n#[test]\nfn function_param_shadows_input_but_global_accessible() {\n    // Function parameter shadows input, but other inputs are still accessible as globals\n    let code = \"\ndef foo(x):\n    return x + y\n\nfoo(100)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned(), \"y\".to_owned()]).unwrap();\n    // x=5, y=3 (inputs), foo(100), inside foo x=100 (param), y=3 (global), returns 103\n    let result = ex\n        .run_no_limits(vec![MontyObject::Int(5), MontyObject::Int(3)])\n        .unwrap();\n    assert_eq!(result, MontyObject::Int(103));\n}\n\n#[test]\nfn function_param_shadows_input_accessible_outside() {\n    // Script input should still be accessible outside the function that shadows it\n    let code = \"\ndef double(x):\n    return x * 2\n\ndouble(10) + x\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    // x=5 (input), double(10) = 20, then 20 + x (global) = 20 + 5 = 25\n    let result = ex.run_no_limits(vec![MontyObject::Int(5)]).unwrap();\n    assert_eq!(result, MontyObject::Int(25));\n}\n\n#[test]\nfn function_param_with_default_shadows_input() {\n    // Function parameter with default should shadow input when called with argument\n    let code = \"\ndef foo(x=100):\n    return x + 1\n\nfoo(x * 2)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    // x=5 (input), foo(10), inside foo x=10 (param), returns 11\n    let result = ex.run_no_limits(vec![MontyObject::Int(5)]).unwrap();\n    assert_eq!(result, MontyObject::Int(11));\n}\n\n#[test]\nfn function_uses_input_as_argument() {\n    // Input can be passed as argument, and param shadows inside function\n    let code = \"\ndef double(x):\n    return x * 2\n\ndouble(x)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    // x=7 (input), double(7), inside double x=7 (param from arg), returns 14\n    let result = ex.run_no_limits(vec![MontyObject::Int(7)]).unwrap();\n    assert_eq!(result, MontyObject::Int(14));\n}\n\n#[test]\nfn function_doesnt_uses_input_as_argument() {\n    let code = \"\ndef double(x):\n    return x * 2\n\ndouble(2)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![\"x\".to_owned()]).unwrap();\n    // x=7 (input), double(7), inside double x=7 (param from arg), returns 14\n    let result = ex.run_no_limits(vec![MontyObject::Int(7)]).unwrap();\n    assert_eq!(result, MontyObject::Int(4));\n}\n"
  },
  {
    "path": "crates/monty/tests/json_serde.rs",
    "content": "//! Tests for JSON serialization and deserialization of `MontyObject`.\n//!\n//! `MontyObject` uses derived serde with externally tagged enum format.\n//! This means each variant is wrapped in an object with the variant name as key.\n\nuse monty::{ExcType, MontyObject, MontyRun};\n\n// === JSON Serialization Tests ===\n\n#[test]\nfn json_output_primitives() {\n    // Primitives are wrapped in their variant names\n    assert_eq!(serde_json::to_string(&MontyObject::Int(42)).unwrap(), r#\"{\"Int\":42}\"#);\n    assert_eq!(\n        serde_json::to_string(&MontyObject::Float(1.5)).unwrap(),\n        r#\"{\"Float\":1.5}\"#\n    );\n    assert_eq!(\n        serde_json::to_string(&MontyObject::String(\"hi\".into())).unwrap(),\n        r#\"{\"String\":\"hi\"}\"#\n    );\n    assert_eq!(\n        serde_json::to_string(&MontyObject::Bool(true)).unwrap(),\n        r#\"{\"Bool\":true}\"#\n    );\n    assert_eq!(serde_json::to_string(&MontyObject::None).unwrap(), r#\"\"None\"\"#);\n}\n\n#[test]\nfn json_output_list() {\n    let ex = MontyRun::new(\"[1, 'two', 3.0]\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(\n        serde_json::to_string(&result).unwrap(),\n        r#\"{\"List\":[{\"Int\":1},{\"String\":\"two\"},{\"Float\":3.0}]}\"#\n    );\n}\n\n#[test]\nfn json_output_dict() {\n    let ex = MontyRun::new(\"{'a': 1, 'b': 2}\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(\n        serde_json::to_string(&result).unwrap(),\n        r#\"{\"Dict\":[[{\"String\":\"a\"},{\"Int\":1}],[{\"String\":\"b\"},{\"Int\":2}]]}\"#\n    );\n}\n\n#[test]\nfn json_output_tuple() {\n    let ex = MontyRun::new(\"(1, 'two')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(\n        serde_json::to_string(&result).unwrap(),\n        r#\"{\"Tuple\":[{\"Int\":1},{\"String\":\"two\"}]}\"#\n    );\n}\n\n#[test]\nfn json_output_bytes() {\n    let ex = MontyRun::new(\"b'hi'\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(serde_json::to_string(&result).unwrap(), r#\"{\"Bytes\":[104,105]}\"#);\n}\n\n#[test]\nfn json_output_ellipsis() {\n    let ex = MontyRun::new(\"...\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(serde_json::to_string(&result).unwrap(), r#\"\"Ellipsis\"\"#);\n}\n\n#[test]\nfn json_output_exception() {\n    let obj = MontyObject::Exception {\n        exc_type: ExcType::ValueError,\n        arg: Some(\"test\".to_string()),\n    };\n    assert_eq!(\n        serde_json::to_string(&obj).unwrap(),\n        r#\"{\"Exception\":{\"exc_type\":\"ValueError\",\"arg\":\"test\"}}\"#\n    );\n}\n\n#[test]\nfn json_output_repr() {\n    let obj = MontyObject::Repr(\"<function foo>\".to_string());\n    assert_eq!(serde_json::to_string(&obj).unwrap(), r#\"{\"Repr\":\"<function foo>\"}\"#);\n}\n\n#[test]\nfn json_output_cycle_list() {\n    // Test JSON serialization of cyclic list\n    let ex = MontyRun::new(\"a = []; a.append(a); a\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    // The cyclic reference becomes MontyObject::Cycle\n    assert_eq!(\n        serde_json::to_string(&result).unwrap(),\n        r#\"{\"List\":[{\"Cycle\":[1,\"[...]\"]}]}\"#\n    );\n}\n\n#[test]\nfn json_output_cycle_dict() {\n    // Test JSON serialization of cyclic dict\n    let ex = MontyRun::new(\"d = {}; d['self'] = d; d\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    assert_eq!(\n        serde_json::to_string(&result).unwrap(),\n        r#\"{\"Dict\":[[{\"String\":\"self\"},{\"Cycle\":[1,\"{...}\"]}]]}\"#\n    );\n}\n\n// === JSON Deserialization Tests ===\n\n#[test]\nfn json_deserialize_primitives() {\n    // Deserialize tagged format\n    let int: MontyObject = serde_json::from_str(r#\"{\"Int\":42}\"#).unwrap();\n    let float: MontyObject = serde_json::from_str(r#\"{\"Float\":2.5}\"#).unwrap();\n    let string: MontyObject = serde_json::from_str(r#\"{\"String\":\"hello\"}\"#).unwrap();\n    let bool_val: MontyObject = serde_json::from_str(r#\"{\"Bool\":true}\"#).unwrap();\n    let null: MontyObject = serde_json::from_str(r#\"\"None\"\"#).unwrap();\n\n    assert_eq!(int, MontyObject::Int(42));\n    assert_eq!(float, MontyObject::Float(2.5));\n    assert_eq!(string, MontyObject::String(\"hello\".to_string()));\n    assert_eq!(bool_val, MontyObject::Bool(true));\n    assert_eq!(null, MontyObject::None);\n}\n\n#[test]\nfn json_deserialize_list() {\n    let list: MontyObject = serde_json::from_str(r#\"{\"List\":[{\"Int\":1},{\"String\":\"two\"},{\"Float\":3.0}]}\"#).unwrap();\n    assert_eq!(\n        list,\n        MontyObject::List(vec![\n            MontyObject::Int(1),\n            MontyObject::String(\"two\".to_string()),\n            MontyObject::Float(3.0)\n        ])\n    );\n}\n\n#[test]\nfn json_deserialize_dict() {\n    let dict: MontyObject =\n        serde_json::from_str(r#\"{\"Dict\":[[{\"String\":\"a\"},{\"Int\":1}],[{\"String\":\"b\"},{\"Int\":2}]]}\"#).unwrap();\n    if let MontyObject::Dict(pairs) = dict {\n        let pairs_vec: Vec<_> = pairs.into_iter().collect();\n        assert_eq!(pairs_vec.len(), 2);\n        assert_eq!(\n            pairs_vec[0],\n            (MontyObject::String(\"a\".to_string()), MontyObject::Int(1))\n        );\n        assert_eq!(\n            pairs_vec[1],\n            (MontyObject::String(\"b\".to_string()), MontyObject::Int(2))\n        );\n    } else {\n        panic!(\"expected Dict\");\n    }\n}\n\n// === Round-trip Tests ===\n\n#[test]\nfn json_roundtrip() {\n    // Values round-trip through JSON correctly\n    let ex = MontyRun::new(\n        \"{'items': [1, 'two', None], 'flag': True}\".to_owned(),\n        \"test.py\",\n        vec![],\n    )\n    .unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let json = serde_json::to_string(&result).unwrap();\n    let parsed: MontyObject = serde_json::from_str(&json).unwrap();\n    assert_eq!(result, parsed);\n}\n\n#[test]\nfn json_roundtrip_empty() {\n    // Empty structures round-trip correctly\n    let list: MontyObject = serde_json::from_str(r#\"{\"List\":[]}\"#).unwrap();\n    let dict: MontyObject = serde_json::from_str(r#\"{\"Dict\":[]}\"#).unwrap();\n    assert_eq!(serde_json::to_string(&list).unwrap(), r#\"{\"List\":[]}\"#);\n    assert_eq!(serde_json::to_string(&dict).unwrap(), r#\"{\"Dict\":[]}\"#);\n}\n\n// === Cycle Equality Tests ===\n\n#[test]\nfn cycle_equality_same_id() {\n    // Multiple references to the same cyclic object should produce equal Cycle values\n    let ex = MontyRun::new(\"a = []; a.append(a); [a, a]\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n\n    if let MontyObject::List(outer) = &result {\n        assert_eq!(outer.len(), 2, \"outer list should have 2 elements\");\n\n        if let (MontyObject::List(inner1), MontyObject::List(inner2)) = (&outer[0], &outer[1]) {\n            assert_eq!(inner1.len(), 1);\n            assert_eq!(inner2.len(), 1);\n            assert_eq!(inner1[0], inner2[0], \"cycles referencing same object should be equal\");\n            assert!(matches!(&inner1[0], MontyObject::Cycle(..)));\n        } else {\n            panic!(\"expected inner lists\");\n        }\n    } else {\n        panic!(\"expected outer list\");\n    }\n}\n\n#[test]\nfn cycle_equality_different_ids() {\n    // Two separate cyclic objects should produce unequal Cycle values\n    let ex = MontyRun::new(\n        \"a = []; a.append(a); b = []; b.append(b); [a, b]\".to_owned(),\n        \"test.py\",\n        vec![],\n    )\n    .unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n\n    if let MontyObject::List(outer) = &result {\n        assert_eq!(outer.len(), 2, \"outer list should have 2 elements\");\n\n        if let (MontyObject::List(inner1), MontyObject::List(inner2)) = (&outer[0], &outer[1]) {\n            assert_eq!(inner1.len(), 1);\n            assert_eq!(inner2.len(), 1);\n            assert_ne!(\n                inner1[0], inner2[0],\n                \"cycles referencing different objects should not be equal\"\n            );\n\n            if let (MontyObject::Cycle(id1, ph1), MontyObject::Cycle(id2, ph2)) = (&inner1[0], &inner2[0]) {\n                assert_ne!(id1, id2, \"heap IDs should differ\");\n                assert_eq!(ph1, ph2, \"placeholders should match (both are lists)\");\n                assert_eq!(*ph1, \"[...]\");\n            } else {\n                panic!(\"expected Cycle variants\");\n            }\n        } else {\n            panic!(\"expected inner lists\");\n        }\n    } else {\n        panic!(\"expected outer list\");\n    }\n}\n"
  },
  {
    "path": "crates/monty/tests/main.rs",
    "content": "use monty::{MontyObject, MontyRun};\n\n/// Test we can reuse exec without borrow checker issues.\n#[test]\nfn repeat_exec() {\n    let ex = MontyRun::new(\"1 + 2\".to_owned(), \"test.py\", vec![]).unwrap();\n\n    let r = ex.run_no_limits(vec![]).unwrap();\n    let int_value: i64 = r.as_ref().try_into().unwrap();\n    assert_eq!(int_value, 3);\n\n    let r = ex.run_no_limits(vec![]).unwrap();\n    let int_value: i64 = r.as_ref().try_into().unwrap();\n    assert_eq!(int_value, 3);\n}\n\n#[test]\nfn test_get_interned_string() {\n    let ex = MontyRun::new(\"'foobar'\".to_owned(), \"test.py\", vec![]).unwrap();\n\n    let r = ex.run_no_limits(vec![]).unwrap();\n    let int_value: String = r.as_ref().try_into().unwrap();\n    assert_eq!(int_value, \"foobar\");\n\n    let r = ex.run_no_limits(vec![]).unwrap();\n    let int_value: String = r.as_ref().try_into().unwrap();\n    assert_eq!(int_value, \"foobar\");\n}\n\n/// Test that calling a method on a dataclass in standard execution mode\n/// (without iter/external function support) returns a NotImplementedError.\n/// This exercises the `FrameExit::MethodCall` path in `frame_exit_to_object`.\n#[test]\nfn dataclass_method_call_in_standard_mode_errors() {\n    let point = MontyObject::Dataclass {\n        name: \"Point\".to_string(),\n        type_id: 0,\n        field_names: vec![\"x\".to_string(), \"y\".to_string()],\n        attrs: vec![\n            (MontyObject::String(\"x\".to_string()), MontyObject::Int(1)),\n            (MontyObject::String(\"y\".to_string()), MontyObject::Int(2)),\n        ]\n        .into(),\n        frozen: true,\n    };\n\n    let ex = MontyRun::new(\"point.sum()\".to_owned(), \"test.py\", vec![\"point\".to_string()]).unwrap();\n\n    let err = ex.run_no_limits(vec![point]).unwrap_err();\n    let msg = err.to_string();\n    assert!(\n        msg.contains(\"Method call 'sum' not implemented with standard execution\"),\n        \"Expected NotImplementedError for method call, got: {msg}\"\n    );\n}\n\n/// Test that subscript augmented matrix multiplication reports the dedicated\n/// unsupported-operation compile error.\n///\n/// CPython supports `@=` syntax, so the comparative Python test-case suite\n/// cannot cover Monty's current compile-time rejection of this operator. Keep\n/// this as a Rust-side regression test until matrix multiplication support\n/// exists.\n#[test]\nfn subscript_augassign_matmul_reports_not_supported() {\n    let err = MontyRun::new(\"d = {'x': 1}\\nd['x'] @= 2\".to_owned(), \"test.py\", vec![]).unwrap_err();\n    assert_eq!(\n        err.to_string(),\n        \"Traceback (most recent call last):\\n  File \\\"test.py\\\", line 2\\n    d['x'] @= 2\\n    ~~~~~~\\nSyntaxError: matrix multiplication augmented assignment (@=) is not yet supported\"\n    );\n}\n"
  },
  {
    "path": "crates/monty/tests/math_module.rs",
    "content": "use monty::{MontyObject, MontyRun};\n\n/// Helper to run a Python expression and return the result.\nfn run_expr(code: &str) -> MontyObject {\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    ex.run_no_limits(vec![]).unwrap()\n}\n\n/// Helper to run Python code that is expected to raise an exception.\n/// Returns the exception message string.\nfn run_expect_error(code: &str) -> String {\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let err = ex.run_no_limits(vec![]).unwrap_err();\n    err.to_string()\n}\n\n// ==========================\n// Overflow tests (i64-specific)\n// ==========================\n\n/// `math.factorial(21)` overflows i64 (21! = 51090942171709440000 > i64::MAX).\n/// Monty raises OverflowError since it doesn't have big integer support.\n#[test]\nfn factorial_i64_overflow() {\n    let msg = run_expect_error(\"import math\\nmath.factorial(21)\");\n    assert!(\n        msg.contains(\"OverflowError\"),\n        \"Expected OverflowError for factorial(21), got: {msg}\"\n    );\n}\n\n/// `math.comb(66, 33)` fits in i64 (7219428434016265740) thanks to GCD reduction\n/// that avoids intermediate overflow. Verify it computes the correct value.\n#[test]\nfn comb_large_but_fits_i64() {\n    let result = run_expr(\"import math\\nmath.comb(66, 33)\");\n    let v: i64 = (&result).try_into().unwrap();\n    assert_eq!(v, 7_219_428_434_016_265_740);\n}\n\n/// `math.comb(68, 34)` overflows i64 even with GCD reduction\n/// (68C34 = 28048800420600 * ... > i64::MAX).\n#[test]\nfn comb_i64_overflow() {\n    let msg = run_expect_error(\"import math\\nmath.comb(68, 34)\");\n    assert!(\n        msg.contains(\"OverflowError\"),\n        \"Expected OverflowError for comb(68, 34), got: {msg}\"\n    );\n}\n\n/// `math.perm(21, 21)` overflows i64 (same as 21! which exceeds i64::MAX).\n#[test]\nfn perm_i64_overflow() {\n    let msg = run_expect_error(\"import math\\nmath.perm(21, 21)\");\n    assert!(\n        msg.contains(\"OverflowError\"),\n        \"Expected OverflowError for perm(21, 21), got: {msg}\"\n    );\n}\n\n// ==========================\n// ldexp negative exponent loop\n// ==========================\n\n/// `math.ldexp(1.0, -1050)` exercises the negative exponent loop in `math_ldexp`\n/// because -1050 is between -1074 and -1022, requiring iterative halving.\n#[test]\nfn ldexp_large_negative_exponent_loop() {\n    let result = run_expr(\"import math\\nmath.ldexp(1.0, -1050)\");\n    let f: f64 = (&result).try_into().unwrap();\n    // ldexp(1.0, -1050) is a very small subnormal but not zero\n    assert!(f > 0.0, \"ldexp(1.0, -1050) should be positive, got: {f}\");\n    assert!(f < 1e-300, \"ldexp(1.0, -1050) should be tiny, got: {f}\");\n}\n\n/// `math.ldexp(1.0, -1074)` is the smallest representable positive float (subnormal).\n#[test]\nfn ldexp_minimum_subnormal() {\n    let result = run_expr(\"import math\\nmath.ldexp(1.0, -1074)\");\n    let f: f64 = (&result).try_into().unwrap();\n    // Compare bits directly since this is an exact IEEE 754 subnormal value\n    assert_eq!(\n        f.to_bits(),\n        5e-324_f64.to_bits(),\n        \"ldexp(1.0, -1074) should equal 5e-324\"\n    );\n}\n\n// ==========================\n// isqrt Newton's method refinement\n// ==========================\n\n/// `math.isqrt` with values near i64::MAX where f64 sqrt loses precision,\n/// triggering the Newton's method refinement and overshoot correction.\n#[test]\nfn isqrt_large_values_newton_refinement() {\n    // i64::MAX = 9223372036854775807\n    // isqrt(i64::MAX) = 3037000499 (3037000499^2 = 9223372030926249001 <= i64::MAX)\n    let result = run_expr(\"import math\\nmath.isqrt(9223372036854775807)\");\n    let v: i64 = (&result).try_into().unwrap();\n    assert_eq!(v, 3_037_000_499);\n\n    // 3037000499^2 = 9223372030926249001 (perfect square)\n    let result = run_expr(\"import math\\nmath.isqrt(9223372030926249001)\");\n    let v: i64 = (&result).try_into().unwrap();\n    assert_eq!(v, 3_037_000_499);\n\n    // 3037000499^2 - 1: the initial f64 estimate overshoots by 1,\n    // triggering both the delta==0 break and the overshoot correction loop.\n    let result = run_expr(\"import math\\nmath.isqrt(9223372030926249000)\");\n    let v: i64 = (&result).try_into().unwrap();\n    assert_eq!(v, 3_037_000_498);\n}\n"
  },
  {
    "path": "crates/monty/tests/name_lookup.rs",
    "content": "//! Tests for `NameLookup` — the mechanism by which the host resolves undefined names\n//! during iterative execution.\n//!\n//! When the VM encounters an undefined global (or unassigned local at module scope),\n//! it yields `RunProgress::NameLookup` so the host can provide a value or signal\n//! that the name is truly undefined. These tests exercise that API directly:\n//!\n//! - Resolving names to various types (functions, ints, strings, lists, booleans)\n//! - Returning `NameLookupResult::Undefined` to trigger `NameError`\n//! - Caching: a resolved name should not yield another `NameLookup`\n//! - Multiple distinct names each get their own lookup\n//! - Builtins bypass the `NameLookup` mechanism entirely\n\nuse monty::{MontyObject, MontyRun, NameLookupResult, NoLimitTracker, PrintWriter, RunProgress};\n\n/// Helper: drives execution through consecutive `NameLookup` yields,\n/// resolving each by calling `resolver(name)`.\nfn resolve_lookups_with(\n    mut progress: RunProgress<NoLimitTracker>,\n    resolver: impl Fn(&str) -> NameLookupResult,\n) -> Result<RunProgress<NoLimitTracker>, monty::MontyException> {\n    while let RunProgress::NameLookup(lookup) = progress {\n        let result = resolver(&lookup.name);\n        progress = lookup.resume(result, PrintWriter::Stdout)?;\n    }\n    Ok(progress)\n}\n\n/// Helper: resolves all `NameLookup` yields as `Function` objects (the common case\n/// for external function calls).\nfn resolve_as_functions(\n    progress: RunProgress<NoLimitTracker>,\n) -> Result<RunProgress<NoLimitTracker>, monty::MontyException> {\n    resolve_lookups_with(progress, |name| {\n        NameLookupResult::Value(MontyObject::Function {\n            name: name.to_string(),\n            docstring: None,\n        })\n    })\n}\n\n// ---------------------------------------------------------------------------\n// Resolving to different types\n// ---------------------------------------------------------------------------\n\n/// NameLookup resolved as a Function → code can call it and use the result.\n#[test]\nfn resolve_as_function_and_call() {\n    let runner = MontyRun::new(\"x = ext(10); x + 1\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    // Resolve NameLookup for 'ext' as a function\n    let progress = resolve_as_functions(progress).unwrap();\n\n    // Should now be at a FunctionCall for ext(10)\n    let call = progress.into_function_call().expect(\"expected FunctionCall\");\n    assert_eq!(call.function_name, \"ext\");\n    assert_eq!(call.args, vec![MontyObject::Int(10)]);\n\n    // Resume with 42 → code evaluates 42 + 1 = 43\n    let result = call.resume(MontyObject::Int(42), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(43));\n}\n\n/// NameLookup resolved as an integer constant — no function call involved.\n#[test]\nfn resolve_as_int() {\n    let runner = MontyRun::new(\"PI + 1\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"PI\");\n\n    let result = lookup.resume(MontyObject::Int(3), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(4));\n}\n\n/// NameLookup resolved as a string value.\n#[test]\nfn resolve_as_string() {\n    let runner = MontyRun::new(\"GREETING + '!'\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"GREETING\");\n\n    let result = lookup\n        .resume(MontyObject::String(\"hello\".to_string()), PrintWriter::Stdout)\n        .unwrap();\n    assert_eq!(\n        result.into_complete().unwrap(),\n        MontyObject::String(\"hello!\".to_string())\n    );\n}\n\n/// NameLookup resolved as a boolean.\n#[test]\nfn resolve_as_bool() {\n    let runner = MontyRun::new(\"not FLAG\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"FLAG\");\n\n    let result = lookup.resume(MontyObject::Bool(true), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Bool(false));\n}\n\n/// NameLookup resolved as a list.\n#[test]\nfn resolve_as_list() {\n    let runner = MontyRun::new(\"len(ITEMS)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"ITEMS\");\n\n    let items = MontyObject::List(vec![MontyObject::Int(10), MontyObject::Int(20), MontyObject::Int(30)]);\n    let result = lookup.resume(items, PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(3));\n}\n\n/// NameLookup resolved as a float.\n#[test]\nfn resolve_as_float() {\n    let runner = MontyRun::new(\"TAU + 0.5\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"TAU\");\n\n    let result = lookup.resume(MontyObject::Float(6.0), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Float(6.5));\n}\n\n// ---------------------------------------------------------------------------\n// Undefined → NameError\n// ---------------------------------------------------------------------------\n\n/// Returning `NameLookupResult::Undefined` causes `NameError` at global scope.\n#[test]\nfn undefined_raises_name_error() {\n    let runner = MontyRun::new(\"unknown_thing\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"unknown_thing\");\n\n    let err = lookup\n        .resume(NameLookupResult::Undefined, PrintWriter::Stdout)\n        .unwrap_err();\n    let msg = err.to_string();\n    assert!(\n        msg.contains(\"NameError: name 'unknown_thing' is not defined\"),\n        \"Expected NameError, got: {msg}\"\n    );\n}\n\n/// In non-iterative mode (`run_no_limits`), undefined globals automatically raise `NameError`\n/// without yielding to the host.\n#[test]\nfn standard_mode_raises_name_error() {\n    let runner = MontyRun::new(\"unknown_fn(42)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let err = runner.run_no_limits(vec![]).unwrap_err();\n    let msg = err.to_string();\n    assert!(\n        msg.contains(\"NameError: name 'unknown_fn' is not defined\"),\n        \"Expected NameError, got: {msg}\"\n    );\n}\n\n/// Undefined inside a function that does NOT assign the name locally should\n/// still raise `NameError` (not `UnboundLocalError`), since the name lookup\n/// falls through to the global scope.\n#[test]\nfn undefined_in_function_raises_name_error() {\n    // `missing` is not assigned inside `f()`, so Python treats it as a global lookup\n    let code = \"def f():\\n    return missing\\nf()\".to_owned();\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"missing\");\n\n    let err = lookup\n        .resume(NameLookupResult::Undefined, PrintWriter::Stdout)\n        .unwrap_err();\n    let msg = err.to_string();\n    assert!(\n        msg.contains(\"NameError: name 'missing' is not defined\"),\n        \"Expected NameError, got: {msg}\"\n    );\n}\n\n// ---------------------------------------------------------------------------\n// Caching\n// ---------------------------------------------------------------------------\n\n/// Function calls in call context bypass `NameLookup` entirely — they go\n/// directly to `FunctionCall` via `LoadGlobalCallable` + `ExtFunction`.\n#[test]\nfn resolved_name_is_cached() {\n    let code = \"a = ext(1); b = ext(2); a + b\".to_owned();\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let mut progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let mut call_count = 0;\n    loop {\n        match progress {\n            RunProgress::FunctionCall(call) => {\n                assert_eq!(call.function_name, \"ext\");\n                call_count += 1;\n                let val: i64 = (&call.args[0]).try_into().unwrap();\n                progress = call.resume(MontyObject::Int(val * 10), PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::Complete(result) => {\n                // ext(1) -> 10, ext(2) -> 20 → 30\n                assert_eq!(result, MontyObject::Int(30));\n                break;\n            }\n            other => panic!(\"unexpected progress: {other:?}\"),\n        }\n    }\n    assert_eq!(call_count, 2, \"should get FunctionCall for each ext() call\");\n}\n\n/// A non-function constant resolved once is also cached.\n#[test]\nfn resolved_constant_is_cached() {\n    // Use the same constant twice — should only yield one NameLookup\n    let code = \"X + X\".to_owned();\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let mut progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let mut lookup_count = 0;\n    loop {\n        match progress {\n            RunProgress::NameLookup(lookup) => {\n                assert_eq!(lookup.name, \"X\");\n                lookup_count += 1;\n                progress = lookup.resume(MontyObject::Int(21), PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::Complete(result) => {\n                assert_eq!(result, MontyObject::Int(42));\n                break;\n            }\n            other => panic!(\"unexpected progress: {other:?}\"),\n        }\n    }\n    assert_eq!(lookup_count, 1, \"constant should be cached after first lookup\");\n}\n\n// ---------------------------------------------------------------------------\n// Multiple names\n// ---------------------------------------------------------------------------\n\n/// Different undefined names in call context each yield `FunctionCall` directly\n/// (via `LoadGlobalCallable`), not `NameLookup`.\n#[test]\nfn multiple_names_each_looked_up() {\n    let code = \"a = foo(1); b = bar(2); a + b\".to_owned();\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let mut progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let mut called_names = Vec::new();\n    loop {\n        match progress {\n            RunProgress::FunctionCall(call) => {\n                called_names.push(call.function_name.clone());\n                let val: i64 = (&call.args[0]).try_into().unwrap();\n                progress = call.resume(MontyObject::Int(val * 100), PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::Complete(result) => {\n                // foo(1) -> 100, bar(2) -> 200 → 300\n                assert_eq!(result, MontyObject::Int(300));\n                break;\n            }\n            other => panic!(\"unexpected progress: {other:?}\"),\n        }\n    }\n    assert_eq!(called_names, vec![\"foo\", \"bar\"]);\n}\n\n/// Mix of function calls and constant name lookups in the same execution.\n/// `ext` in call context goes directly to `FunctionCall` (no `NameLookup`).\n/// `OFFSET` in non-call context yields `NameLookup`.\n#[test]\nfn mixed_function_and_constant_lookups() {\n    let code = \"ext(OFFSET)\".to_owned();\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let mut progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    let mut looked_up_names = Vec::new();\n    loop {\n        match progress {\n            RunProgress::NameLookup(lookup) => {\n                let name = lookup.name.clone();\n                looked_up_names.push(name.clone());\n                let value = match name.as_str() {\n                    \"OFFSET\" => MontyObject::Int(100),\n                    _ => panic!(\"unexpected name lookup: {name}\"),\n                };\n                progress = lookup.resume(value, PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::FunctionCall(call) => {\n                // ext goes directly to FunctionCall via LoadGlobalCallable\n                assert_eq!(call.function_name, \"ext\");\n                assert_eq!(call.args, vec![MontyObject::Int(100)]);\n                progress = call.resume(MontyObject::Int(999), PrintWriter::Stdout).unwrap();\n            }\n            RunProgress::Complete(result) => {\n                assert_eq!(result, MontyObject::Int(999));\n                break;\n            }\n            other => panic!(\"unexpected progress: {other:?}\"),\n        }\n    }\n    // Only 'OFFSET' yields NameLookup; 'ext' bypasses it via LoadGlobalCallable\n    assert_eq!(looked_up_names, vec![\"OFFSET\"]);\n}\n\n// ---------------------------------------------------------------------------\n// Builtins bypass NameLookup\n// ---------------------------------------------------------------------------\n\n/// Known builtins like `len` and `range` do NOT trigger `NameLookup`.\n#[test]\nfn builtins_do_not_trigger_lookup() {\n    let runner = MontyRun::new(\"len([1, 2, 3])\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n    assert_eq!(progress.into_complete().unwrap(), MontyObject::Int(3));\n}\n\n/// `range` is a builtin — should complete without any NameLookup.\n#[test]\nfn range_builtin_no_lookup() {\n    let runner = MontyRun::new(\"list(range(3))\".to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n    assert_eq!(\n        progress.into_complete().unwrap(),\n        MontyObject::List(vec![MontyObject::Int(0), MontyObject::Int(1), MontyObject::Int(2)])\n    );\n}\n\n// ---------------------------------------------------------------------------\n// Function passed as input (no NameLookup)\n// ---------------------------------------------------------------------------\n\n/// A function passed as an input is already in the namespace — calling it should\n/// yield a `FunctionCall` directly without any `NameLookup`.\n#[test]\nfn input_function_no_lookup() {\n    let runner = MontyRun::new(\"my_fn(10)\".to_owned(), \"test.py\", vec![\"my_fn\".to_string()]).unwrap();\n\n    let progress = runner\n        .start(\n            vec![MontyObject::Function {\n                name: \"my_fn\".to_string(),\n                docstring: None,\n            }],\n            NoLimitTracker,\n            PrintWriter::Stdout,\n        )\n        .unwrap();\n\n    // Should go straight to FunctionCall — no NameLookup\n    let call = progress\n        .into_function_call()\n        .expect(\"expected FunctionCall, not NameLookup\");\n    assert_eq!(call.function_name, \"my_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(10)]);\n\n    let result = call.resume(MontyObject::Int(99), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(99));\n}\n\n/// A function input assigned to a new variable and called via the alias should\n/// still yield a `FunctionCall` without any `NameLookup`.\n#[test]\nfn input_function_reassigned_then_called() {\n    let runner = MontyRun::new(\n        \"alias = my_fn; alias(5)\".to_owned(),\n        \"test.py\",\n        vec![\"my_fn\".to_string()],\n    )\n    .unwrap();\n\n    let progress = runner\n        .start(\n            vec![MontyObject::Function {\n                name: \"my_fn\".to_string(),\n                docstring: None,\n            }],\n            NoLimitTracker,\n            PrintWriter::Stdout,\n        )\n        .unwrap();\n\n    // No NameLookup — my_fn is an input, alias is a local assignment\n    let call = progress\n        .into_function_call()\n        .expect(\"expected FunctionCall, not NameLookup\");\n    assert_eq!(call.function_name, \"my_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(5)]);\n\n    let result = call.resume(MontyObject::Int(50), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(50));\n}\n\n/// A function input used alongside a name-looked-up constant: the function should\n/// not trigger NameLookup but the constant should.\n#[test]\nfn input_function_with_looked_up_arg() {\n    let runner = MontyRun::new(\"my_fn(OFFSET)\".to_owned(), \"test.py\", vec![\"my_fn\".to_string()]).unwrap();\n\n    let mut progress = runner\n        .start(\n            vec![MontyObject::Function {\n                name: \"my_fn\".to_string(),\n                docstring: None,\n            }],\n            NoLimitTracker,\n            PrintWriter::Stdout,\n        )\n        .unwrap();\n\n    // OFFSET is undefined — should yield NameLookup (my_fn should NOT)\n    let lookup = match progress {\n        RunProgress::NameLookup(l) => l,\n        other => panic!(\"expected NameLookup for 'OFFSET', got {other:?}\"),\n    };\n    assert_eq!(lookup.name, \"OFFSET\");\n    progress = lookup.resume(MontyObject::Int(42), PrintWriter::Stdout).unwrap();\n\n    // Now should be at FunctionCall for my_fn(42)\n    let call = progress.into_function_call().expect(\"expected FunctionCall\");\n    assert_eq!(call.function_name, \"my_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(42)]);\n\n    let result = call.resume(MontyObject::Int(100), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(100));\n}\n\n/// When a NameLookup resolves to a Function whose name differs from the variable\n/// name (i.e., the function's `__name__` is not interned), the VM stores it as\n/// `HeapData::ExtFunction(String)`. Calling it should yield a `FunctionCall` with\n/// the function's actual name, not the variable name.\n#[test]\nfn resolve_function_with_non_interned_name() {\n    // `x = foobar` triggers NameLookup for 'foobar', we resolve it as a function\n    // named 'not_foobar'. Then `x()` calls the function.\n    let code = \"x = foobar; x()\".to_owned();\n    let runner = MontyRun::new(code, \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    // First: NameLookup for 'foobar'\n    let lookup = progress.into_name_lookup().unwrap();\n    assert_eq!(lookup.name, \"foobar\");\n\n    // Resolve with a function whose name is NOT 'foobar' — it won't be interned\n    let progress = lookup\n        .resume(\n            NameLookupResult::Value(MontyObject::Function {\n                name: \"not_foobar\".to_string(),\n                docstring: None,\n            }),\n            PrintWriter::Stdout,\n        )\n        .unwrap();\n\n    // The VM calls x() which is HeapData::ExtFunction(\"not_foobar\") → FunctionCall\n    let call = progress\n        .into_function_call()\n        .expect(\"expected FunctionCall for 'not_foobar'\");\n    assert_eq!(call.function_name, \"not_foobar\");\n    assert!(call.args.is_empty());\n    assert!(call.kwargs.is_empty());\n\n    // Resume with a return value\n    let result = call.resume(MontyObject::Int(42), PrintWriter::Stdout).unwrap();\n    assert_eq!(result.into_complete().unwrap(), MontyObject::Int(42));\n}\n"
  },
  {
    "path": "crates/monty/tests/os_tests.rs",
    "content": "//! Tests for OS function calls.\n//!\n//! Verifies that Path filesystem methods and os module functions yield\n//! `RunProgress::OsCall` with the correct `OsFunction` variant and arguments,\n//! and that return values are correctly used by Python code.\n\nuse monty::{MontyObject, MontyRun, NoLimitTracker, OsFunction, PrintWriter, RunProgress, file_stat};\n\n/// Helper to run code and extract the OsCall progress.\n///\n/// Runs the provided Python code and asserts that it yields an `OsCall`.\n/// Returns the `OsFunction` and positional arguments from the call.\n/// The state is resumed with a mock result to properly clean up ref counts.\nfn run_to_oscall(code: &str) -> (OsFunction, Vec<MontyObject>) {\n    let runner = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    match progress {\n        RunProgress::OsCall(call) => {\n            // Resume with a mock result appropriate for the function type.\n            let mock_result = match call.function {\n                OsFunction::Exists | OsFunction::IsFile | OsFunction::IsDir | OsFunction::IsSymlink => {\n                    MontyObject::Bool(true)\n                }\n                OsFunction::ReadText | OsFunction::Resolve | OsFunction::Absolute => {\n                    MontyObject::String(\"mock\".to_owned())\n                }\n                OsFunction::ReadBytes => MontyObject::Bytes(vec![]),\n                OsFunction::Stat => MontyObject::None,\n                OsFunction::Iterdir => MontyObject::List(vec![]),\n                OsFunction::WriteText\n                | OsFunction::WriteBytes\n                | OsFunction::Mkdir\n                | OsFunction::Unlink\n                | OsFunction::Rmdir\n                | OsFunction::Rename => MontyObject::None,\n                OsFunction::Getenv => MontyObject::String(\"mock_env_value\".to_owned()),\n                OsFunction::GetEnviron => MontyObject::Dict(vec![].into()),\n            };\n            let function = call.function;\n            let args = call.args.clone();\n            let _ = call.resume(mock_result, PrintWriter::Stdout);\n            (function, args)\n        }\n        _ => panic!(\"expected OsCall, got {progress:?}\"),\n    }\n}\n\n/// Helper to run code, provide an OS call result, and get the final value.\nfn run_oscall_with_result(code: &str, mock_result: MontyObject) -> (OsFunction, Vec<MontyObject>, MontyObject) {\n    let runner = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let progress = runner.start(vec![], NoLimitTracker, PrintWriter::Stdout).unwrap();\n\n    match progress {\n        RunProgress::OsCall(call) => {\n            let function = call.function;\n            let args = call.args.clone();\n            let resumed = call.resume(mock_result, PrintWriter::Stdout).unwrap();\n            let final_result = resumed.into_complete().expect(\"expected Complete after resume\");\n            (function, args, final_result)\n        }\n        _ => panic!(\"expected OsCall, got {progress:?}\"),\n    }\n}\n\n// =============================================================================\n// Verify each OsFunction variant yields correctly\n// =============================================================================\n\n#[test]\nfn path_exists() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp/test.txt').exists()\");\n    assert_eq!(func, OsFunction::Exists);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp/test.txt\".to_owned())]);\n}\n\n#[test]\nfn path_is_file() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp/test.txt').is_file()\");\n    assert_eq!(func, OsFunction::IsFile);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp/test.txt\".to_owned())]);\n}\n\n#[test]\nfn path_is_dir() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp').is_dir()\");\n    assert_eq!(func, OsFunction::IsDir);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp\".to_owned())]);\n}\n\n#[test]\nfn path_is_symlink() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp/link').is_symlink()\");\n    assert_eq!(func, OsFunction::IsSymlink);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp/link\".to_owned())]);\n}\n\n#[test]\nfn path_read_text() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp/file.txt').read_text()\");\n    assert_eq!(func, OsFunction::ReadText);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp/file.txt\".to_owned())]);\n}\n\n#[test]\nfn path_read_bytes() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp/file.bin').read_bytes()\");\n    assert_eq!(func, OsFunction::ReadBytes);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp/file.bin\".to_owned())]);\n}\n\n#[test]\nfn path_stat() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp/file.txt').stat()\");\n    assert_eq!(func, OsFunction::Stat);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp/file.txt\".to_owned())]);\n}\n\n#[test]\nfn path_iterdir() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/tmp').iterdir()\");\n    assert_eq!(func, OsFunction::Iterdir);\n    assert_eq!(args, vec![MontyObject::Path(\"/tmp\".to_owned())]);\n}\n\n#[test]\nfn path_resolve() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('./relative').resolve()\");\n    assert_eq!(func, OsFunction::Resolve);\n    assert_eq!(args, vec![MontyObject::Path(\"./relative\".to_owned())]);\n}\n\n#[test]\nfn path_absolute() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('./relative').absolute()\");\n    assert_eq!(func, OsFunction::Absolute);\n    assert_eq!(args, vec![MontyObject::Path(\"./relative\".to_owned())]);\n}\n\n// =============================================================================\n// Path argument handling (spaces, unicode, concatenation)\n// =============================================================================\n\n#[test]\nfn path_with_spaces() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/path/with spaces/file.txt').exists()\");\n    assert_eq!(func, OsFunction::Exists);\n    assert_eq!(args[0], MontyObject::Path(\"/path/with spaces/file.txt\".to_owned()));\n}\n\n#[test]\nfn path_with_unicode() {\n    let (func, args) = run_to_oscall(\"from pathlib import Path; Path('/путь/文件.txt').exists()\");\n    assert_eq!(func, OsFunction::Exists);\n    assert_eq!(args[0], MontyObject::Path(\"/путь/文件.txt\".to_owned()));\n}\n\n#[test]\nfn path_concatenation_yields_correct_path() {\n    let (func, args) = run_to_oscall(\n        r\"\nfrom pathlib import Path\nbase = Path('/home')\nfull = base / 'user' / 'file.txt'\nfull.exists()\n\",\n    );\n    assert_eq!(func, OsFunction::Exists);\n    assert_eq!(args[0], MontyObject::Path(\"/home/user/file.txt\".to_owned()));\n}\n\n// =============================================================================\n// Round-trip tests: OS call result used by Python code\n// =============================================================================\n\n#[test]\nfn exists_result_used_in_conditional() {\n    let code = r\"\nfrom pathlib import Path\n'found' if Path('/tmp/test.txt').exists() else 'missing'\n\";\n    let (func, _, result) = run_oscall_with_result(code, MontyObject::Bool(true));\n    assert_eq!(func, OsFunction::Exists);\n    assert_eq!(result, MontyObject::String(\"found\".to_owned()));\n\n    // Also test false case\n    let (_, _, result) = run_oscall_with_result(code, MontyObject::Bool(false));\n    assert_eq!(result, MontyObject::String(\"missing\".to_owned()));\n}\n\n#[test]\nfn read_text_result_concatenated() {\n    let code = r\"\nfrom pathlib import Path\n'Content: ' + Path('/tmp/hello.txt').read_text()\n\";\n    let (func, _, result) = run_oscall_with_result(code, MontyObject::String(\"Hello!\".to_owned()));\n    assert_eq!(func, OsFunction::ReadText);\n    assert_eq!(result, MontyObject::String(\"Content: Hello!\".to_owned()));\n}\n\n#[test]\nfn read_bytes_result_used() {\n    let code = r\"\nfrom pathlib import Path\ndata = Path('/tmp/file.bin').read_bytes()\ndata[0]\n\";\n    let (func, _, result) = run_oscall_with_result(code, MontyObject::Bytes(vec![0x42, 0x43, 0x44]));\n    assert_eq!(func, OsFunction::ReadBytes);\n    assert_eq!(result, MontyObject::Int(0x42));\n}\n\n#[test]\nfn iterdir_result_iterated() {\n    let code = r\"\nfrom pathlib import Path\nentries = Path('/tmp').iterdir()\nlen(entries)\n\";\n    // Return a list of path strings (simulating directory entries)\n    let mock_entries = MontyObject::List(vec![\n        MontyObject::String(\"/tmp/file1.txt\".to_owned()),\n        MontyObject::String(\"/tmp/file2.txt\".to_owned()),\n        MontyObject::String(\"/tmp/subdir\".to_owned()),\n    ]);\n    let (func, args, result) = run_oscall_with_result(code, mock_entries);\n\n    assert_eq!(func, OsFunction::Iterdir);\n    assert_eq!(args[0], MontyObject::Path(\"/tmp\".to_owned()));\n    assert_eq!(result, MontyObject::Int(3));\n}\n\n#[test]\nfn iterdir_result_indexed() {\n    let code = r\"\nfrom pathlib import Path\nentries = Path('/home/user').iterdir()\nentries[0]\n\";\n    let mock_entries = MontyObject::List(vec![\n        MontyObject::String(\"/home/user/documents\".to_owned()),\n        MontyObject::String(\"/home/user/downloads\".to_owned()),\n    ]);\n    let (func, args, result) = run_oscall_with_result(code, mock_entries);\n\n    assert_eq!(func, OsFunction::Iterdir);\n    assert_eq!(args[0], MontyObject::Path(\"/home/user\".to_owned()));\n    assert_eq!(result, MontyObject::String(\"/home/user/documents\".to_owned()));\n}\n\n#[test]\nfn stat_result_st_size() {\n    let code = r\"\nfrom pathlib import Path\ninfo = Path('/tmp/file.txt').stat()\ninfo.st_size\n\";\n    let (func, args, result) = run_oscall_with_result(code, file_stat(0o644, 1024, 0.0));\n\n    assert_eq!(func, OsFunction::Stat);\n    assert_eq!(args[0], MontyObject::Path(\"/tmp/file.txt\".to_owned()));\n    assert_eq!(result, MontyObject::Int(1024));\n}\n\n#[test]\nfn stat_result_st_mode() {\n    let code = r\"\nfrom pathlib import Path\ninfo = Path('/tmp/file.txt').stat()\ninfo.st_mode\n\";\n    // 0o755 = rwxr-xr-x (file_stat adds 0o100_000 for regular file type)\n    let (func, args, result) = run_oscall_with_result(code, file_stat(0o755, 0, 0.0));\n\n    assert_eq!(func, OsFunction::Stat);\n    assert_eq!(args[0], MontyObject::Path(\"/tmp/file.txt\".to_owned()));\n    assert_eq!(result, MontyObject::Int(0o100_755));\n}\n\n#[test]\nfn stat_result_multiple_fields() {\n    let code = r\"\nfrom pathlib import Path\ninfo = Path('/var/log/syslog').stat()\n(info.st_size, info.st_mode)\n\";\n    // 0o644 = rw-r--r-- (file_stat adds 0o100_000 for regular file type)\n    let (func, args, result) = run_oscall_with_result(code, file_stat(0o644, 4096, 0.0));\n\n    assert_eq!(func, OsFunction::Stat);\n    assert_eq!(args[0], MontyObject::Path(\"/var/log/syslog\".to_owned()));\n    assert_eq!(\n        result,\n        MontyObject::Tuple(vec![MontyObject::Int(4096), MontyObject::Int(0o100_644)])\n    );\n}\n\n#[test]\nfn stat_result_index_access() {\n    // stat_result also supports index access like a tuple\n    let code = r\"\nfrom pathlib import Path\ninfo = Path('/tmp/file.txt').stat()\ninfo[6]  # st_size is at index 6\n\";\n    let (func, args, result) = run_oscall_with_result(code, file_stat(0o644, 2048, 0.0));\n\n    assert_eq!(func, OsFunction::Stat);\n    assert_eq!(args[0], MontyObject::Path(\"/tmp/file.txt\".to_owned()));\n    assert_eq!(result, MontyObject::Int(2048));\n}\n\n// =============================================================================\n// os.getenv tests\n// =============================================================================\n\n#[test]\nfn os_getenv_yields_oscall() {\n    let code = r\"\nimport os\nos.getenv('PATH')\n\";\n    let (func, args) = run_to_oscall(code);\n    assert_eq!(func, OsFunction::Getenv);\n    // First arg is key, second is default (None if not provided)\n    assert_eq!(args[0], MontyObject::String(\"PATH\".to_owned()));\n    assert_eq!(args[1], MontyObject::None);\n}\n\n#[test]\nfn os_getenv_with_default() {\n    let code = r\"\nimport os\nos.getenv('MISSING', 'fallback')\n\";\n    let (func, args) = run_to_oscall(code);\n    assert_eq!(func, OsFunction::Getenv);\n    assert_eq!(args[0], MontyObject::String(\"MISSING\".to_owned()));\n    assert_eq!(args[1], MontyObject::String(\"fallback\".to_owned()));\n}\n\n#[test]\nfn os_getenv_result_used() {\n    let code = r\"\nimport os\n'HOME=' + os.getenv('HOME')\n\";\n    let (func, _, result) = run_oscall_with_result(code, MontyObject::String(\"/home/user\".to_owned()));\n    assert_eq!(func, OsFunction::Getenv);\n    assert_eq!(result, MontyObject::String(\"HOME=/home/user\".to_owned()));\n}\n\n// =============================================================================\n// os.environ tests\n// =============================================================================\n\n#[test]\nfn os_environ_yields_oscall() {\n    let code = r\"\nimport os\nos.environ\n\";\n    let (func, args) = run_to_oscall(code);\n    assert_eq!(func, OsFunction::GetEnviron);\n    // GetEnviron takes no arguments\n    assert!(args.is_empty(), \"expected empty args, got {args:?}\");\n}\n\n#[test]\nfn os_environ_result_is_dict() {\n    let code = r\"\nimport os\ntype(os.environ).__name__\n\";\n    let mock_env = MontyObject::Dict(\n        vec![\n            (\n                MontyObject::String(\"HOME\".to_owned()),\n                MontyObject::String(\"/home/user\".to_owned()),\n            ),\n            (\n                MontyObject::String(\"PATH\".to_owned()),\n                MontyObject::String(\"/usr/bin\".to_owned()),\n            ),\n        ]\n        .into(),\n    );\n    let (func, _, result) = run_oscall_with_result(code, mock_env);\n    assert_eq!(func, OsFunction::GetEnviron);\n    assert_eq!(result, MontyObject::String(\"dict\".to_owned()));\n}\n\n#[test]\nfn os_environ_key_access() {\n    let code = r\"\nimport os\nos.environ['HOME']\n\";\n    let mock_env = MontyObject::Dict(\n        vec![(\n            MontyObject::String(\"HOME\".to_owned()),\n            MontyObject::String(\"/home/user\".to_owned()),\n        )]\n        .into(),\n    );\n    let (func, _, result) = run_oscall_with_result(code, mock_env);\n    assert_eq!(func, OsFunction::GetEnviron);\n    assert_eq!(result, MontyObject::String(\"/home/user\".to_owned()));\n}\n\n#[test]\nfn os_environ_get_method() {\n    let code = r\"\nimport os\nos.environ.get('MISSING', 'default')\n\";\n    let mock_env = MontyObject::Dict(vec![].into());\n    let (func, _, result) = run_oscall_with_result(code, mock_env);\n    assert_eq!(func, OsFunction::GetEnviron);\n    assert_eq!(result, MontyObject::String(\"default\".to_owned()));\n}\n\n#[test]\nfn os_environ_len() {\n    let code = r\"\nimport os\nlen(os.environ)\n\";\n    let mock_env = MontyObject::Dict(\n        vec![\n            (MontyObject::String(\"A\".to_owned()), MontyObject::String(\"1\".to_owned())),\n            (MontyObject::String(\"B\".to_owned()), MontyObject::String(\"2\".to_owned())),\n            (MontyObject::String(\"C\".to_owned()), MontyObject::String(\"3\".to_owned())),\n        ]\n        .into(),\n    );\n    let (func, _, result) = run_oscall_with_result(code, mock_env);\n    assert_eq!(func, OsFunction::GetEnviron);\n    assert_eq!(result, MontyObject::Int(3));\n}\n\n#[test]\nfn os_environ_in_check() {\n    let code = r\"\nimport os\n'HOME' in os.environ\n\";\n    let mock_env = MontyObject::Dict(\n        vec![(\n            MontyObject::String(\"HOME\".to_owned()),\n            MontyObject::String(\"/home/user\".to_owned()),\n        )]\n        .into(),\n    );\n    let (func, _, result) = run_oscall_with_result(code, mock_env);\n    assert_eq!(func, OsFunction::GetEnviron);\n    assert_eq!(result, MontyObject::Bool(true));\n}\n"
  },
  {
    "path": "crates/monty/tests/parse_errors.rs",
    "content": "use std::fmt::Write;\n\nuse monty::{ExcType, MontyException, MontyRun};\n\n/// Helper to extract the exception type from a parse error.\nfn get_exc_type(result: Result<MontyRun, MontyException>) -> ExcType {\n    let err = result.expect_err(\"expected parse error\");\n    err.exc_type()\n}\n\n#[test]\nfn complex_numbers_return_not_implemented_error() {\n    let result = MontyRun::new(\"1 + 2j\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::NotImplementedError);\n}\n\n#[test]\nfn complex_numbers_have_descriptive_message() {\n    let result = MontyRun::new(\"1 + 2j\".to_owned(), \"test.py\", vec![]);\n    let exc = result.expect_err(\"expected parse error\");\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"complex\")),\n        \"message should mention 'complex', got: {exc}\"\n    );\n}\n\n#[test]\nfn yield_expressions_return_not_implemented_error() {\n    // Yield expressions are not supported and fail at parse time\n    let result = MontyRun::new(\"def foo():\\n    yield 1\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::NotImplementedError);\n    let result = MontyRun::new(\"def foo():\\n    yield 1\".to_owned(), \"test.py\", vec![]);\n    let exc = result.expect_err(\"expected parse error\");\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"yield\")),\n        \"message should mention 'yield', got: {exc}\"\n    );\n}\n\n#[test]\nfn classes_return_not_implemented_error() {\n    let result = MontyRun::new(\"class Foo: pass\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::NotImplementedError);\n}\n\n#[test]\nfn unknown_imports_compile_successfully_error_deferred_to_runtime() {\n    // Unknown modules (not sys, typing, os, etc.) compile successfully.\n    // The ModuleNotFoundError is deferred to runtime, allowing TYPE_CHECKING\n    // imports to work without causing compile-time errors.\n    let result = MontyRun::new(\"import foobar\".to_owned(), \"test.py\", vec![]);\n    assert!(result.is_ok(), \"unknown import should compile successfully\");\n}\n\n#[test]\nfn with_statement_returns_not_implemented_error() {\n    let result = MontyRun::new(\"with open('f') as f: pass\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::NotImplementedError);\n}\n\n#[test]\nfn error_display_format() {\n    // Verify the Display format matches Python's exception output with traceback\n    let result = MontyRun::new(\"1 + 2j\".to_owned(), \"test.py\", vec![]);\n    let err = result.expect_err(\"expected parse error\");\n    let display = err.to_string();\n    // Should start with traceback header\n    assert!(\n        display.starts_with(\"Traceback (most recent call last):\"),\n        \"display should start with 'Traceback': got: {display}\"\n    );\n    // Should contain the file/line info\n    assert!(\n        display.contains(\"File \\\"test.py\\\", line 1\"),\n        \"display should contain file location, got: {display}\"\n    );\n    // Should end with NotImplementedError message\n    assert!(\n        display.contains(\"NotImplementedError:\"),\n        \"display should contain 'NotImplementedError:', got: {display}\"\n    );\n    assert!(\n        display.contains(\"monty syntax parser\"),\n        \"display should mention 'monty syntax parser', got: {display}\"\n    );\n}\n\n/// Tests that syntax errors return `SyntaxError` exceptions.\n\n#[test]\nfn invalid_fstring_format_spec_returns_syntax_error() {\n    let result = MontyRun::new(\"f'{1:10xyz}'\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn invalid_fstring_format_spec_str_returns_syntax_error() {\n    let result = MontyRun::new(\"f'{\\\"hello\\\":abc}'\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn syntax_error_display_format() {\n    let result = MontyRun::new(\"f'{1:10xyz}'\".to_owned(), \"test.py\", vec![]);\n    let err = result.expect_err(\"expected parse error\");\n    let display = err.to_string();\n    assert!(\n        display.contains(\"SyntaxError:\"),\n        \"display should contain 'SyntaxError:', got: {display}\"\n    );\n}\n\n#[test]\nfn deeply_nested_tuples_exceed_limit() {\n    // Build nested tuple like ((((x,),),),) with depth > 200\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"({code},)\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    let err = result.expect_err(\"expected parse error\");\n    assert_eq!(err.exc_type(), ExcType::SyntaxError);\n    assert_eq!(\n        err.message(),\n        Some(\"too many nested parentheses\"),\n        \"error message should match CPython, got: {:?}\",\n        err.message()\n    );\n}\n\n#[test]\nfn nested_tuples_within_limit_succeed() {\n    // Build nested tuple with depth = 20, which is well under the 200 limit.\n    // We use a small value because the ruff parser uses significant stack\n    // space per nesting level in debug builds.\n    let mut code = \"x\".to_string();\n    for _ in 0..20 {\n        code = format!(\"({code},)\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert!(result.is_ok(), \"nesting within limit should succeed\");\n}\n\n#[test]\nfn deeply_nested_unpack_assignment_exceeds_limit() {\n    // Build nested unpack assignment like ((((x,),),),) = value with depth > 200\n    let mut target = \"x\".to_string();\n    for _ in 0..250 {\n        target = format!(\"({target},)\");\n    }\n    let code = format!(\"{target} = (1,)\");\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    let err = result.expect_err(\"expected parse error\");\n    assert_eq!(err.exc_type(), ExcType::SyntaxError);\n    assert_eq!(\n        err.message(),\n        Some(\"too many nested parentheses\"),\n        \"error message should match CPython, got: {:?}\",\n        err.message()\n    );\n}\n\n#[test]\nfn deeply_nested_lists_exceed_limit() {\n    // Build nested list like [[[[[x]]]]]\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"[{code}]\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_dicts_exceed_limit() {\n    // Build nested dict like {'a': {'a': {'a': ...}}}\n    let mut code = \"1\".to_string();\n    for _ in 0..250 {\n        code = format!(\"{{'a': {code}}}\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_function_calls_exceed_limit() {\n    // Build nested calls like f(f(f(f(x))))\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"f({code})\");\n    }\n    let code = format!(\"def f(x): return x\\n{code}\");\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_binary_ops_exceed_limit() {\n    // Build nested binary ops like ((((x + 1) + 1) + 1) + 1)\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"({code} + 1)\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_ternary_if_exceed_limit() {\n    // Build nested ternary like (1 if (1 if (1 if ... else 0) else 0) else 0)\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"(1 if {code} else 0)\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_subscripts_exceed_limit() {\n    // Build nested subscripts like a[b[c[d[...]]]]\n    let mut code = \"0\".to_string();\n    for _ in 0..250 {\n        code = format!(\"a[{code}]\");\n    }\n    let code = format!(\"a = [1]\\n{code}\");\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_list_comprehension_exceed_limit() {\n    // Build nested list comprehension like [x for x in [y for y in [...]]]\n    let mut code = \"[1]\".to_string();\n    for _ in 0..250 {\n        code = format!(\"[x for x in {code}]\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_if_statements_exceed_limit() {\n    // Build nested if statements\n    let mut code = \"x = 1\\n\".to_string();\n    for i in 0..250 {\n        let indent = \"    \".repeat(i);\n        writeln!(code, \"{indent}if 1:\").unwrap();\n    }\n    write!(code, \"{}pass\", \"    \".repeat(250)).unwrap();\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_while_loops_exceed_limit() {\n    // Build nested while loops\n    let mut code = String::new();\n    for i in 0..250 {\n        let indent = \"    \".repeat(i);\n        writeln!(code, \"{indent}while True:\").unwrap();\n    }\n    write!(code, \"{}break\", \"    \".repeat(250)).unwrap();\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_for_loops_exceed_limit() {\n    // Build nested for loops\n    let mut code = String::new();\n    for i in 0..250 {\n        let indent = \"    \".repeat(i);\n        writeln!(code, \"{indent}for x in [1]:\").unwrap();\n    }\n    write!(code, \"{}pass\", \"    \".repeat(250)).unwrap();\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_try_except_exceed_limit() {\n    // Build nested try/except blocks\n    let mut code = String::new();\n    for i in 0..250 {\n        let indent = \"    \".repeat(i);\n        writeln!(code, \"{indent}try:\").unwrap();\n    }\n    writeln!(code, \"{}pass\", \"    \".repeat(250)).unwrap();\n    for i in (0..250).rev() {\n        let indent = \"    \".repeat(i);\n        writeln!(code, \"{indent}except: pass\").unwrap();\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_function_defs_exceed_limit() {\n    // Build nested function definitions\n    let mut code = String::new();\n    for i in 0..250 {\n        let indent = \"    \".repeat(i);\n        writeln!(code, \"{indent}def f():\").unwrap();\n    }\n    write!(code, \"{}pass\", \"    \".repeat(250)).unwrap();\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_attribute_access_exceed_limit() {\n    // Build chained attribute access like a.b.c.d.e...\n    let mut code = \"a\".to_string();\n    for _ in 0..250 {\n        code.push_str(\".x\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_lambdas_exceed_limit() {\n    // Build nested lambdas like (lambda: (lambda: (lambda: ... x)))\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"(lambda: {code})\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_unary_not_exceed_limit() {\n    // Build nested not operators like not (not (not ... True))\n    let mut code = \"True\".to_string();\n    for _ in 0..250 {\n        code = format!(\"not ({code})\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_unary_minus_exceed_limit() {\n    // Build nested unary minus like -(-(-... 1))\n    let mut code = \"1\".to_string();\n    for _ in 0..250 {\n        code = format!(\"-({code})\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_walrus_operator_exceed_limit() {\n    // Build nested walrus operators like (a := (b := (c := ... 1)))\n    let mut code = \"1\".to_string();\n    for i in 0..250 {\n        code = format!(\"(x{i} := {code})\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_await_exceed_limit() {\n    // Build nested await like await (await (await ... x))\n    // We need this in an async function context\n    let mut code = \"x\".to_string();\n    for _ in 0..250 {\n        code = format!(\"await ({code})\");\n    }\n    let code = format!(\"async def f():\\n    {code}\");\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_boolean_and_exceed_limit() {\n    // Build nested boolean and like (True and (True and (True and ...)))\n    let mut code = \"True\".to_string();\n    for _ in 0..250 {\n        code = format!(\"(True and {code})\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn deeply_nested_boolean_or_exceed_limit() {\n    // Build nested boolean or like (False or (False or (False or ...)))\n    let mut code = \"True\".to_string();\n    for _ in 0..250 {\n        code = format!(\"(False or {code})\");\n    }\n    let result = MontyRun::new(code, \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n// === Runtime NotImplementedError tests ===\n// These test that unimplemented features return proper errors instead of panicking.\n\n/// Helper to run code and get the exception type from a runtime error.\nfn run_and_get_exc_type(code: &str) -> ExcType {\n    let runner = MontyRun::new(code.to_owned(), \"test.py\", vec![]).expect(\"should parse\");\n    let err = runner.run_no_limits(vec![]).expect_err(\"expected runtime error\");\n    err.exc_type()\n}\n\n#[test]\nfn matrix_multiplication_returns_not_implemented_error() {\n    // The @ operator (matrix multiplication) is not supported at runtime\n    assert_eq!(run_and_get_exc_type(\"1 @ 2\"), ExcType::NotImplementedError);\n}\n\n#[test]\nfn matrix_multiplication_augmented_assignment_returns_syntax_error() {\n    // The @= operator (augmented matrix multiplication) is not supported at compile time\n    let result = MontyRun::new(\"a = 1\\na @= 2\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::SyntaxError);\n}\n\n#[test]\nfn matrix_multiplication_augmented_assignment_has_descriptive_message() {\n    // Verify the error message is helpful\n    let result = MontyRun::new(\"a = 1\\na @= 2\".to_owned(), \"test.py\", vec![]);\n    let exc = result.expect_err(\"expected compile error\");\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"@=\")),\n        \"message should mention '@=', got: {:?}\",\n        exc.message()\n    );\n}\n\n#[test]\nfn del_statement_returns_not_implemented_error() {\n    // The del statement is not supported at parse time\n    let result = MontyRun::new(\"x = 1\\ndel x\".to_owned(), \"test.py\", vec![]);\n    assert_eq!(get_exc_type(result), ExcType::NotImplementedError);\n}\n"
  },
  {
    "path": "crates/monty/tests/print_writer.rs",
    "content": "use monty::{MontyRun, NoLimitTracker, PrintWriter};\n\n#[test]\nfn print_single_string() {\n    let ex = MontyRun::new(\"print('hello')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"hello\\n\");\n}\n\n#[test]\nfn print_multiple_args() {\n    let ex = MontyRun::new(\"print('hello', 'world')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"hello world\\n\");\n}\n\n#[test]\nfn print_multiple_statements() {\n    let ex = MontyRun::new(\n        \"print('one')\\nprint('two')\\nprint('three')\".to_owned(),\n        \"test.py\",\n        vec![],\n    )\n    .unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"one\\ntwo\\nthree\\n\");\n}\n\n#[test]\nfn print_empty() {\n    let ex = MontyRun::new(\"print()\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"\\n\");\n}\n\n#[test]\nfn print_integers() {\n    let ex = MontyRun::new(\"print(1, 2, 3)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"1 2 3\\n\");\n}\n\n#[test]\nfn print_mixed_types() {\n    let ex = MontyRun::new(\"print('count:', 42, True)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"count: 42 True\\n\");\n}\n\n#[test]\nfn print_in_function() {\n    let code = \"\ndef greet(name):\n    print('Hello', name)\n\ngreet('Alice')\ngreet('Bob')\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"Hello Alice\\nHello Bob\\n\");\n}\n\n#[test]\nfn print_in_loop() {\n    let code = \"\nfor i in range(3):\n    print(i)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"0\\n1\\n2\\n\");\n}\n\n#[test]\nfn collect_output_accessible_after_run() {\n    let ex = MontyRun::new(\"print('test')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"test\\n\");\n}\n\n#[test]\nfn writer_reuse_accumulates() {\n    let mut output = String::new();\n\n    let ex1 = MontyRun::new(\"print('first')\".to_owned(), \"test.py\", vec![]).unwrap();\n    ex1.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n\n    let ex2 = MontyRun::new(\"print('second')\".to_owned(), \"test.py\", vec![]).unwrap();\n    ex2.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n\n    assert_eq!(output, \"first\\nsecond\\n\");\n}\n\n#[test]\nfn disabled_suppresses_output() {\n    let code = \"\nfor i in range(100):\n    print('this should be suppressed', i)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    // Should complete without error, output is silently discarded\n    let result = ex.run(vec![], NoLimitTracker, PrintWriter::Disabled);\n    assert!(result.is_ok());\n}\n\n// === print() kwargs tests ===\n\n#[test]\nfn print_custom_sep() {\n    let ex = MontyRun::new(\"print('a', 'b', 'c', sep='-')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"a-b-c\\n\");\n}\n\n#[test]\nfn print_custom_end() {\n    let ex = MontyRun::new(\"print('hello', end='!')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"hello!\");\n}\n\n#[test]\nfn print_custom_sep_and_end() {\n    let ex = MontyRun::new(\n        \"print('x', 'y', 'z', sep=', ', end='\\\\n---\\\\n')\".to_owned(),\n        \"test.py\",\n        vec![],\n    )\n    .unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"x, y, z\\n---\\n\");\n}\n\n#[test]\nfn print_empty_sep() {\n    let ex = MontyRun::new(\"print('a', 'b', 'c', sep='')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"abc\\n\");\n}\n\n#[test]\nfn print_empty_end() {\n    let code = \"print('first', end='')\\nprint('second')\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"firstsecond\\n\");\n}\n\n#[test]\nfn print_sep_none() {\n    // sep=None should use default space\n    let ex = MontyRun::new(\"print('a', 'b', sep=None)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    // In Python, sep=None means use default, but we treat it as empty string for simplicity\n    // This matches: print('a', 'b', sep=None) outputs \"ab\\n\" with our impl\n    assert_eq!(output, \"a b\\n\");\n}\n\n#[test]\nfn print_end_none() {\n    // end=None should use empty string (our interpretation)\n    let ex = MontyRun::new(\"print('hello', end=None)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"hello\\n\");\n}\n\n#[test]\nfn print_flush_ignored() {\n    // flush=True should be accepted but ignored\n    let ex = MontyRun::new(\"print('test', flush=True)\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"test\\n\");\n}\n\n#[test]\nfn print_kwargs_dict() {\n    // Use a dict literal instead of dict() since dict builtin is not implemented\n    let ex = MontyRun::new(\"print('a', 'b', **{'sep': '-'})\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"a-b\\n\");\n}\n\n#[test]\nfn print_only_kwargs_no_args() {\n    let ex = MontyRun::new(\"print(sep='-', end='!')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"!\");\n}\n\n#[test]\nfn print_multiline_sep() {\n    let ex = MontyRun::new(\"print(1, 2, 3, sep='\\\\n')\".to_owned(), \"test.py\", vec![]).unwrap();\n    let mut output = String::new();\n    ex.run(vec![], NoLimitTracker, PrintWriter::Collect(&mut output))\n        .unwrap();\n    assert_eq!(output, \"1\\n2\\n3\\n\");\n}\n"
  },
  {
    "path": "crates/monty/tests/py_object.rs",
    "content": "use monty::MontyObject;\n\n/// Tests for `MontyObject::is_truthy()` - Python's truth value testing rules.\n\n#[test]\nfn is_truthy_none_is_falsy() {\n    assert!(!MontyObject::None.is_truthy());\n}\n\n#[test]\nfn is_truthy_ellipsis_is_truthy() {\n    assert!(MontyObject::Ellipsis.is_truthy());\n}\n\n#[test]\nfn is_truthy_false_is_falsy() {\n    assert!(!MontyObject::Bool(false).is_truthy());\n}\n\n#[test]\nfn is_truthy_true_is_truthy() {\n    assert!(MontyObject::Bool(true).is_truthy());\n}\n\n#[test]\nfn is_truthy_zero_int_is_falsy() {\n    assert!(!MontyObject::Int(0).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonzero_int_is_truthy() {\n    assert!(MontyObject::Int(1).is_truthy());\n    assert!(MontyObject::Int(-1).is_truthy());\n    assert!(MontyObject::Int(42).is_truthy());\n}\n\n#[test]\nfn is_truthy_zero_float_is_falsy() {\n    assert!(!MontyObject::Float(0.0).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonzero_float_is_truthy() {\n    assert!(MontyObject::Float(1.0).is_truthy());\n    assert!(MontyObject::Float(-0.5).is_truthy());\n    assert!(MontyObject::Float(f64::INFINITY).is_truthy());\n}\n\n#[test]\nfn is_truthy_empty_string_is_falsy() {\n    assert!(!MontyObject::String(String::new()).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonempty_string_is_truthy() {\n    assert!(MontyObject::String(\"hello\".to_string()).is_truthy());\n    assert!(MontyObject::String(\" \".to_string()).is_truthy());\n}\n\n#[test]\nfn is_truthy_empty_bytes_is_falsy() {\n    assert!(!MontyObject::Bytes(vec![]).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonempty_bytes_is_truthy() {\n    assert!(MontyObject::Bytes(vec![0]).is_truthy());\n    assert!(MontyObject::Bytes(vec![1, 2, 3]).is_truthy());\n}\n\n#[test]\nfn is_truthy_empty_list_is_falsy() {\n    assert!(!MontyObject::List(vec![]).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonempty_list_is_truthy() {\n    assert!(MontyObject::List(vec![MontyObject::Int(1)]).is_truthy());\n}\n\n#[test]\nfn is_truthy_empty_tuple_is_falsy() {\n    assert!(!MontyObject::Tuple(vec![]).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonempty_tuple_is_truthy() {\n    assert!(MontyObject::Tuple(vec![MontyObject::Int(1)]).is_truthy());\n}\n\n#[test]\nfn is_truthy_empty_dict_is_falsy() {\n    assert!(!MontyObject::dict(vec![]).is_truthy());\n}\n\n#[test]\nfn is_truthy_nonempty_dict_is_truthy() {\n    let dict = vec![(MontyObject::String(\"key\".to_string()), MontyObject::Int(1))];\n    assert!(MontyObject::dict(dict).is_truthy());\n}\n\n/// Tests for `MontyObject::type_name()` - Python type names.\n\n#[test]\nfn type_name() {\n    assert_eq!(MontyObject::None.type_name(), \"NoneType\");\n    assert_eq!(MontyObject::Ellipsis.type_name(), \"ellipsis\");\n    assert_eq!(MontyObject::Bool(true).type_name(), \"bool\");\n    assert_eq!(MontyObject::Bool(false).type_name(), \"bool\");\n    assert_eq!(MontyObject::Int(0).type_name(), \"int\");\n    assert_eq!(MontyObject::Int(42).type_name(), \"int\");\n    assert_eq!(MontyObject::Float(0.0).type_name(), \"float\");\n    assert_eq!(MontyObject::Float(2.5).type_name(), \"float\");\n    assert_eq!(MontyObject::String(String::new()).type_name(), \"str\");\n    assert_eq!(MontyObject::String(\"hello\".to_string()).type_name(), \"str\");\n    assert_eq!(MontyObject::Bytes(vec![]).type_name(), \"bytes\");\n    assert_eq!(MontyObject::Bytes(vec![1, 2, 3]).type_name(), \"bytes\");\n    assert_eq!(MontyObject::List(vec![]).type_name(), \"list\");\n    assert_eq!(MontyObject::Tuple(vec![]).type_name(), \"tuple\");\n    assert_eq!(MontyObject::dict(vec![]).type_name(), \"dict\");\n}\n"
  },
  {
    "path": "crates/monty/tests/regex.rs",
    "content": "/// Tests for regex-specific behavior that differs from CPython.\n///\n/// These tests verify Monty-specific regex behavior that cannot be tested via\n/// the datatest runner (which runs tests against both CPython and Monty).\n/// In particular, `fancy_regex` enforces a backtrack limit that CPython lacks,\n/// so pathological patterns raise `PatternError` in Monty instead of hanging.\n///\n/// CPython's regex engine uses backtracking with no step limit. Pathological\n/// patterns (e.g. `((a+)\\2)+b` against 50+ 'a's) cause exponential-time hangs\n/// that grow unboundedly — a denial-of-service vector. Monty uses `fancy_regex`\n/// which enforces a default 1M-step backtrack limit, raising `re.PatternError`\n/// when exceeded. This is strictly better behavior for a sandbox.\nuse monty::MontyRun;\n\n/// Helper to run Python code and return the string result.\nfn run(code: &str) -> String {\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let s: String = result.as_ref().try_into().unwrap();\n    s\n}\n\n/// Verify that `fancy_regex`'s backtrack limit prevents ReDoS.\n///\n/// CPython's regex engine has no backtrack limit, so pathological patterns with\n/// backreferences cause exponential-time hangs (e.g. `((a+)\\2)+b` against 40 'a's\n/// takes ~0.17s on CPython and doubles with each additional character, making it\n/// completely unusable at ~50+ characters and a denial-of-service vector).\n///\n/// Monty uses `fancy_regex` which enforces a default 1M-step backtrack limit.\n/// Patterns that exceed this limit raise `re.PatternError` instead of hanging,\n/// making the sandbox safe against ReDoS attacks via backreference-based patterns.\n///\n/// Note: `fancy_regex` delegates simple patterns (no backreferences or lookaround)\n/// to the `regex` crate's DFA engine, which guarantees linear-time matching.\n/// The backtrack limit only applies to patterns that require the backtracking engine.\n#[test]\nfn backtrack_limit_prevents_redos() {\n    // Pattern with backreference forces the backtracking engine.\n    // ((a+)\\2)+b tries to match repeated groups of a's where each group\n    // is followed by its own backreference, then a 'b' that never appears.\n    // This creates exponential backtracking paths.\n    let result = run(r\"\nimport re\ntry:\n    re.search(r'((a+)\\2)+b', 'a' * 40 + 'c')\n    result = 'no error'\nexcept re.PatternError as e:\n    result = str(e)\nresult\n\");\n    assert_eq!(\n        result,\n        \"Error executing regex: Max limit for backtracking count exceeded\"\n    );\n}\n\n/// Verify that the backtrack limit also applies to compiled patterns.\n#[test]\nfn backtrack_limit_on_compiled_pattern() {\n    let result = run(r\"\nimport re\np = re.compile(r'((a+)\\2)+b')\ntry:\n    p.search('a' * 40 + 'c')\n    result = 'no error'\nexcept re.PatternError as e:\n    result = str(e)\nresult\n\");\n    assert_eq!(\n        result,\n        \"Error executing regex: Max limit for backtracking count exceeded\"\n    );\n}\n\n/// Verify that non-fancy patterns (no backreferences/lookaround) are delegated\n/// to the DFA engine and don't hit the backtrack limit even with large inputs.\n#[test]\nfn dfa_engine_handles_large_inputs() {\n    // (a+)+b is pathological for backtracking engines but fancy_regex delegates\n    // it to the regex crate's DFA engine since it has no fancy features.\n    let result = run(r\"\nimport re\nm = re.search(r'(a+)+b', 'a' * 10000 + 'c')\nassert m is None, 'no match expected'\n'ok'\n\");\n    assert_eq!(result, \"ok\");\n}\n"
  },
  {
    "path": "crates/monty/tests/repl.rs",
    "content": "//! Tests for stateful REPL execution with no replay.\n//!\n//! The REPL session keeps heap/global namespace state between snippets and executes\n//! only the newly fed snippet each time.\n\nuse monty::{\n    ExtFunctionResult, MontyException, MontyObject, MontyRepl, NoLimitTracker, PrintWriter, ReplContinuationMode,\n    ReplProgress, ReplStartError, ResourceTracker, detect_repl_continuation_mode,\n};\n\n#[test]\nfn repl_executes_only_new_code() {\n    let mut repl = MontyRepl::new(\"repl.py\", NoLimitTracker);\n    let init_output = feed_run_print(&mut repl, \"counter = 0\").unwrap();\n    assert_eq!(init_output, MontyObject::None);\n\n    // Execute a snippet that mutates state.\n    let output = feed_run_print(&mut repl, \"counter = counter + 1\").unwrap();\n    assert_eq!(output, MontyObject::None);\n\n    // Feed only the read expression. If replay happened, we'd get 2 instead of 1.\n    let output = feed_run_print(&mut repl, \"counter\").unwrap();\n    assert_eq!(output, MontyObject::Int(1));\n}\n\nfn feed_run_print(repl: &mut MontyRepl<impl ResourceTracker>, code: &str) -> Result<MontyObject, MontyException> {\n    repl.feed_run(code, vec![], PrintWriter::Stdout)\n}\n\nfn init_repl(code: &str) -> (MontyRepl<NoLimitTracker>, MontyObject) {\n    let mut repl = MontyRepl::new(\"repl.py\", NoLimitTracker);\n    let output = feed_run_print(&mut repl, code).unwrap();\n    (repl, output)\n}\n\n#[test]\nfn repl_persists_state_and_definitions() {\n    let (mut repl, _) = init_repl(\"x = 10\");\n\n    feed_run_print(&mut repl, \"def add(v):\\n    return x + v\").unwrap();\n    feed_run_print(&mut repl, \"x = 20\").unwrap();\n    let output = feed_run_print(&mut repl, \"add(22)\").unwrap();\n    assert_eq!(output, MontyObject::Int(42));\n}\n\n#[test]\nfn repl_function_redefinition_uses_latest_definition() {\n    let (mut repl, init_output) = init_repl(\"\");\n    assert_eq!(init_output, MontyObject::None);\n\n    feed_run_print(&mut repl, \"def f():\\n    return 1\").unwrap();\n    assert_eq!(feed_run_print(&mut repl, \"f()\").unwrap(), MontyObject::Int(1));\n\n    feed_run_print(&mut repl, \"def f():\\n    return 2\").unwrap();\n    assert_eq!(feed_run_print(&mut repl, \"f()\").unwrap(), MontyObject::Int(2));\n}\n\n#[test]\nfn repl_nested_function_redefinition_updates_callers() {\n    let (mut repl, init_output) = init_repl(\"\");\n    assert_eq!(init_output, MontyObject::None);\n\n    feed_run_print(&mut repl, \"def g():\\n    return 10\").unwrap();\n    feed_run_print(&mut repl, \"def f():\\n    return g() + 1\").unwrap();\n    assert_eq!(feed_run_print(&mut repl, \"f()\").unwrap(), MontyObject::Int(11));\n\n    feed_run_print(&mut repl, \"def g():\\n    return 41\").unwrap();\n    assert_eq!(feed_run_print(&mut repl, \"f()\").unwrap(), MontyObject::Int(42));\n}\n\n#[test]\nfn repl_runtime_error_keeps_partial_state_consistent() {\n    let (mut repl, init_output) = init_repl(\"\");\n    assert_eq!(init_output, MontyObject::None);\n\n    let result = feed_run_print(&mut repl, \"def f():\\n    return 41\\nx = 1\\nraise RuntimeError('boom')\");\n    assert!(result.is_err(), \"snippet should raise RuntimeError\");\n\n    // Definitions and assignments that happened before the exception should remain valid.\n    assert_eq!(feed_run_print(&mut repl, \"f()\").unwrap(), MontyObject::Int(41));\n    assert_eq!(feed_run_print(&mut repl, \"x\").unwrap(), MontyObject::Int(1));\n}\n\n#[test]\nfn repl_heap_mutations_are_not_replayed() {\n    let (mut repl, _) = init_repl(\"items = []\");\n\n    feed_run_print(&mut repl, \"items.append(1)\").unwrap();\n    assert_eq!(\n        feed_run_print(&mut repl, \"items\").unwrap(),\n        MontyObject::List(vec![MontyObject::Int(1)])\n    );\n\n    feed_run_print(&mut repl, \"items.append(2)\").unwrap();\n    assert_eq!(\n        feed_run_print(&mut repl, \"items\").unwrap(),\n        MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])\n    );\n}\n\n#[test]\nfn repl_detects_continuation_mode_for_common_cases() {\n    assert_eq!(\n        detect_repl_continuation_mode(\"value = 1\\n\"),\n        ReplContinuationMode::Complete\n    );\n    assert_eq!(\n        detect_repl_continuation_mode(\"if True:\\n\"),\n        ReplContinuationMode::IncompleteBlock\n    );\n    assert_eq!(\n        detect_repl_continuation_mode(\"[1,\\n\"),\n        ReplContinuationMode::IncompleteImplicit\n    );\n}\n\n#[test]\nfn repl_tracebacks_use_incrementing_python_input_filenames() {\n    let (mut repl, init_output) = init_repl(\"\");\n    assert_eq!(init_output, MontyObject::None);\n\n    let first = feed_run_print(&mut repl, \"missing_name\").unwrap_err();\n    let second = feed_run_print(&mut repl, \"missing_name\").unwrap_err();\n\n    assert_eq!(first.traceback().len(), 1);\n    assert_eq!(second.traceback().len(), 1);\n    assert_eq!(first.traceback()[0].filename, \"<python-input-0>\");\n    assert_eq!(second.traceback()[0].filename, \"<python-input-1>\");\n}\n\n#[test]\nfn repl_dump_load_survives_between_snippets() {\n    let (mut repl, _) = init_repl(\"total = 1\");\n    feed_run_print(&mut repl, \"total = total + 1\").unwrap();\n\n    let bytes = repl.dump().unwrap();\n    let mut loaded: MontyRepl<NoLimitTracker> = MontyRepl::load(&bytes).unwrap();\n\n    feed_run_print(&mut loaded, \"total = total * 21\").unwrap();\n    let output = feed_run_print(&mut loaded, \"total\").unwrap();\n    assert_eq!(output, MontyObject::Int(42));\n}\n\n#[test]\nfn repl_dump_load_preserves_heap_aliasing() {\n    let (mut repl, _) = init_repl(\"a = []\\nb = a\");\n\n    feed_run_print(&mut repl, \"a.append(1)\").unwrap();\n\n    let bytes = repl.dump().unwrap();\n    let mut loaded: MontyRepl<NoLimitTracker> = MontyRepl::load(&bytes).unwrap();\n\n    feed_run_print(&mut loaded, \"b.append(2)\").unwrap();\n    assert_eq!(\n        feed_run_print(&mut loaded, \"a\").unwrap(),\n        MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])\n    );\n    assert_eq!(\n        feed_run_print(&mut loaded, \"b\").unwrap(),\n        MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])\n    );\n}\n\n#[test]\nfn repl_start_external_call_resumes_to_updated_repl() {\n    let (repl, init_output) = init_repl(\"\");\n    assert_eq!(init_output, MontyObject::None);\n\n    // With LoadGlobalCallable, function calls go directly to FunctionCall\n    let progress = repl.feed_start(\"ext_fn(41) + 1\", vec![], PrintWriter::Stdout).unwrap();\n    let call = progress.into_function_call().expect(\"expected function call\");\n    assert_eq!(call.function_name, \"ext_fn\");\n    assert_eq!(call.args, vec![MontyObject::Int(41)]);\n\n    let progress = call.resume(MontyObject::Int(41), PrintWriter::Stdout).unwrap();\n    let (mut repl, value) = progress.into_complete().expect(\"expected completion\");\n    assert_eq!(value, MontyObject::Int(42));\n    assert_eq!(feed_run_print(&mut repl, \"x = 5\").unwrap(), MontyObject::None);\n    assert_eq!(feed_run_print(&mut repl, \"x\").unwrap(), MontyObject::Int(5));\n}\n\n#[test]\nfn repl_progress_dump_load_roundtrip() {\n    let (repl, _) = init_repl(\"\");\n\n    // With LoadGlobalCallable, ext_fn goes directly to FunctionCall\n    let progress = repl.feed_start(\"ext_fn(20) + 22\", vec![], PrintWriter::Stdout).unwrap();\n\n    let bytes = progress.dump().unwrap();\n    let loaded: ReplProgress<NoLimitTracker> = ReplProgress::load(&bytes).unwrap();\n\n    let call = loaded.into_function_call().expect(\"expected function call\");\n    assert_eq!(call.args, vec![MontyObject::Int(20)]);\n\n    let progress = call.resume(MontyObject::Int(20), PrintWriter::Stdout).unwrap();\n    let (mut repl, value) = progress.into_complete().expect(\"expected completion\");\n    assert_eq!(value, MontyObject::Int(42));\n    assert_eq!(feed_run_print(&mut repl, \"z = 1\").unwrap(), MontyObject::None);\n    assert_eq!(feed_run_print(&mut repl, \"z\").unwrap(), MontyObject::Int(1));\n}\n\n#[test]\nfn repl_start_run_pending_resolve_futures_roundtrip() {\n    let (mut repl, _) = init_repl(\"\");\n    feed_run_print(\n        &mut repl,\n        r\"\nasync def main():\n    value = await foo()\n    return value + 1\n\",\n    )\n    .unwrap();\n\n    let progress = repl.feed_start(\"await main()\", vec![], PrintWriter::Stdout).unwrap();\n    // With LoadGlobalCallable, foo() goes directly to FunctionCall\n    let call = progress.into_function_call().expect(\"expected function call\");\n    let call_id = call.call_id;\n\n    let progress = call.resume_pending(PrintWriter::Stdout).unwrap();\n    let bytes = progress.dump().unwrap();\n    let loaded: ReplProgress<NoLimitTracker> = ReplProgress::load(&bytes).unwrap();\n    let state = loaded.into_resolve_futures().expect(\"expected resolve futures\");\n    assert_eq!(state.pending_call_ids(), &[call_id]);\n\n    let progress = state\n        .resume(\n            vec![(call_id, ExtFunctionResult::Return(MontyObject::Int(41)))],\n            PrintWriter::Stdout,\n        )\n        .unwrap();\n    let (mut repl, value) = progress.into_complete().expect(\"expected completion\");\n    assert_eq!(value, MontyObject::Int(42));\n    assert_eq!(\n        feed_run_print(&mut repl, \"final_value = 42\").unwrap(),\n        MontyObject::None\n    );\n    assert_eq!(feed_run_print(&mut repl, \"final_value\").unwrap(), MontyObject::Int(42));\n}\n\n#[test]\nfn repl_start_runtime_error_preserves_repl_state() {\n    // Simulate an agent loop: create variables, then a later snippet raises.\n    // The REPL must survive so subsequent snippets can access prior variables.\n    let (repl, _) = init_repl(\"x = 10\");\n\n    // Snippet that sets a new variable then raises — returned via ReplStartError.\n    let err = repl\n        .feed_start(\"y = 20\\nraise ValueError('boom')\", vec![], PrintWriter::Stdout)\n        .expect_err(\"expected ReplStartError\");\n    let ReplStartError { mut repl, error } = *err;\n    assert_eq!(error.exc_type(), monty::ExcType::ValueError);\n    assert_eq!(error.message(), Some(\"boom\"));\n\n    // Variables from BEFORE the error snippet survive.\n    assert_eq!(feed_run_print(&mut repl, \"x\").unwrap(), MontyObject::Int(10));\n    // Variable assigned BEFORE the raise within the erroring snippet also survives.\n    assert_eq!(feed_run_print(&mut repl, \"y\").unwrap(), MontyObject::Int(20));\n    // New snippets continue to work normally.\n    assert_eq!(feed_run_print(&mut repl, \"x + y + 12\").unwrap(), MontyObject::Int(42));\n}\n\n#[test]\nfn repl_start_runtime_error_during_external_call_preserves_repl_state() {\n    // An external function returns an error, which should come back as ReplStartError\n    // with the REPL session preserved.\n    let (repl, _) = init_repl(\"z = 99\");\n\n    let progress = repl.feed_start(\"ext_fn(1)\", vec![], PrintWriter::Stdout).unwrap();\n    let call = progress.into_function_call().expect(\"expected function call\");\n\n    // Resume with an exception from the external function.\n    let exc = monty::MontyException::new(monty::ExcType::RuntimeError, Some(\"ext failed\".to_string()));\n    let err = call\n        .resume(ExtFunctionResult::Error(exc), PrintWriter::Stdout)\n        .expect_err(\"expected ReplStartError\");\n    let ReplStartError { mut repl, error } = *err;\n    assert_eq!(error.exc_type(), monty::ExcType::RuntimeError);\n\n    // Variable from before the error is still accessible.\n    assert_eq!(feed_run_print(&mut repl, \"z\").unwrap(), MontyObject::Int(99));\n}\n\n#[test]\nfn repl_dataclass_method_call_yields_function_call_with_method_flag() {\n    // Create a REPL with a dataclass input and call a method on it.\n    // This exercises the MethodCall path in repl.rs handle_repl_vm_result.\n    let point = MontyObject::Dataclass {\n        name: \"Point\".to_string(),\n        type_id: 0,\n        field_names: vec![\"x\".to_string(), \"y\".to_string()],\n        attrs: vec![\n            (MontyObject::String(\"x\".to_string()), MontyObject::Int(1)),\n            (MontyObject::String(\"y\".to_string()), MontyObject::Int(2)),\n        ]\n        .into(),\n        frozen: true,\n    };\n\n    let repl = MontyRepl::new(\"repl.py\", NoLimitTracker);\n\n    // Calling point.sum() should yield a FunctionCall with method_call=true.\n    // Pass the dataclass as an input to feed_start() so it gets a namespace slot.\n    let progress = repl\n        .feed_start(\"point.sum()\", vec![(\"point\".to_string(), point)], PrintWriter::Stdout)\n        .unwrap();\n    let call = progress.into_function_call().expect(\"expected method call\");\n\n    assert_eq!(call.function_name, \"sum\");\n    assert!(call.method_call, \"should be a method call\");\n    // First arg should be the dataclass instance (self)\n    assert!(matches!(&call.args[0], MontyObject::Dataclass { name, .. } if name == \"Point\"));\n\n    // Resume with a return value (sum of x + y = 3)\n    let progress = call.resume(MontyObject::Int(3), PrintWriter::Stdout).unwrap();\n    let (mut repl, value) = progress.into_complete().expect(\"expected completion\");\n    assert_eq!(value, MontyObject::Int(3));\n\n    // Verify REPL state is preserved after method call\n    assert_eq!(feed_run_print(&mut repl, \"1 + 1\").unwrap(), MontyObject::Int(2));\n}\n\n#[test]\nfn repl_start_new_external_function_in_later_block() {\n    // Verify that an external function never referenced in prior blocks can be\n    // called for the first time in a later REPL snippet.\n    let (mut repl, _) = init_repl(\"x = 10\");\n\n    feed_run_print(&mut repl, \"y = x + 5\").unwrap();\n\n    // Now call a brand-new external function that was never mentioned before.\n    let progress = repl.feed_start(\"new_ext(y)\", vec![], PrintWriter::Stdout).unwrap();\n    let call = progress.into_function_call().expect(\"expected function call\");\n    assert_eq!(call.function_name, \"new_ext\");\n    assert_eq!(call.args, vec![MontyObject::Int(15)]);\n\n    let progress = call.resume(MontyObject::Int(100), PrintWriter::Stdout).unwrap();\n    let (mut repl, value) = progress.into_complete().expect(\"expected completion\");\n    assert_eq!(value, MontyObject::Int(100));\n\n    // REPL state from before the external call is still intact.\n    assert_eq!(feed_run_print(&mut repl, \"x\").unwrap(), MontyObject::Int(10));\n    assert_eq!(feed_run_print(&mut repl, \"y\").unwrap(), MontyObject::Int(15));\n}\n"
  },
  {
    "path": "crates/monty/tests/resource_limits.rs",
    "content": "/// Tests for resource limits and garbage collection.\n///\n/// These tests verify that the `ResourceTracker` system correctly enforces\n/// allocation limits, time limits, and triggers garbage collection.\nuse std::time::{Duration, Instant};\n\nuse monty::{\n    ExcType, LimitedTracker, MontyObject, MontyRun, NameLookupResult, PrintWriter, ResourceLimits, RunProgress,\n};\n\n/// Resolves consecutive `NameLookup` yields by providing a `Function` object for each name.\n///\n/// External functions are no longer declared upfront. Instead, the VM yields `NameLookup`\n/// when it encounters an unresolved name. This helper resolves all such lookups until\n/// a different progress variant is reached.\nfn resolve_name_lookups<T: monty::ResourceTracker>(\n    mut progress: RunProgress<T>,\n) -> Result<RunProgress<T>, monty::MontyException> {\n    while let RunProgress::NameLookup(lookup) = progress {\n        let name = lookup.name.clone();\n        progress = lookup.resume(\n            NameLookupResult::Value(MontyObject::Function { name, docstring: None }),\n            PrintWriter::Stdout,\n        )?;\n    }\n    Ok(progress)\n}\n\n/// Test that GC properly collects dict cycles via the has_refs() check in allocate().\n///\n/// This test creates cycles using dict literals and dict setitem. Dict setitem\n/// does NOT call mark_potential_cycle(), so the ONLY way may_have_cycles gets\n/// set is through the has_refs() check when allocating a dict with refs.\n///\n/// If has_refs() is disabled, this test will FAIL because GC never runs.\n#[test]\n#[cfg(feature = \"ref-count-return\")]\nfn gc_collects_dict_cycles_via_has_refs() {\n    // Create 200,001 dict cycles. Each iteration:\n    // - Creates empty dict d1\n    // - Creates dict d2 = {'ref': d1} - d2 is allocated WITH a ref to d1\n    //   This triggers has_refs() which sets may_have_cycles = true\n    // - Sets d1['ref'] = d2 - creates cycle d1 <-> d2\n    //   Dict setitem does NOT call mark_potential_cycle()\n    // - On next iteration, both dicts are reassigned, making the cycle unreachable\n    //\n    // GC runs every 100,000 allocations. With 200,001 iterations:\n    // - GC runs at 100k (collects cycles 0-49,999 approximately)\n    // - GC runs at 200k (collects more cycles)\n    // After GC runs, only the final cycle should remain.\n    let code = r\"\n# Create many dict cycles\nfor i in range(200001):\n    d1 = {}\n    d2 = {'ref': d1}  # d2 allocated WITH ref - has_refs() must trigger here\n    d1['ref'] = d2    # Cycle formed - dict setitem does NOT call mark_potential_cycle\n\n# Create final result (not a cycle)\nresult = 'done'\nresult\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let output = ex.run_ref_counts(vec![]).expect(\"should succeed\");\n\n    // GC_INTERVAL is 100,000. With 200,001 iterations creating dict cycles,\n    // GC must have run at least once, resetting allocations_since_gc.\n    // If may_have_cycles was never set (has_refs() disabled), GC never runs\n    // and allocations_since_gc would be ~400k (2 dicts per iteration).\n    assert!(\n        output.allocations_since_gc < 100_000,\n        \"GC should have run (has_refs() must set may_have_cycles): allocations_since_gc = {}\",\n        output.allocations_since_gc\n    );\n\n    // Verify that GC collected most cycles.\n    // If GC failed to collect cycles, heap_count would be >> 400k.\n    // We allow a small number of extra objects for implementation details.\n    assert!(\n        output.heap_count < 20,\n        \"GC should collect most unreachable dict cycles: {} heap objects (expected < 20)\",\n        output.heap_count\n    );\n}\n\n/// Test that GC properly collects self-referencing list cycles.\n///\n/// This test creates cycles using list.append(), which calls mark_potential_cycle().\n/// This tests the mutation-based cycle detection path.\n#[test]\n#[cfg(feature = \"ref-count-return\")]\nfn gc_collects_list_cycles() {\n    // Create 200,001 self-referencing list cycles. Each iteration:\n    // - Creates empty list `a`\n    // - Appends `a` to itself (creating a self-reference cycle)\n    //   This calls mark_potential_cycle() and sets may_have_cycles = true\n    // - On next iteration, `a` is reassigned, making the cycle unreachable\n    //\n    // GC runs every 100,000 allocations. With 200,001 iterations:\n    // - GC runs at 100k (collects cycles 0-99,999)\n    // - GC runs at 200k (collects cycles 100k-199,999)\n    // After GC runs, only the final cycle should remain.\n    let code = r\"\n# Create many self-referencing list cycles\nfor i in range(200001):\n    a = []\n    a.append(a)  # Creates cycle via list.append() which calls mark_potential_cycle()\n\n# Create final result (not a cycle)\nresult = [1, 2, 3]\nlen(result)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let output = ex.run_ref_counts(vec![]).expect(\"should succeed\");\n\n    // GC_INTERVAL is 100,000. With 200,001 iterations creating list cycles,\n    // GC must have run at least twice, resetting allocations_since_gc.\n    assert!(\n        output.allocations_since_gc < 100_000,\n        \"GC should have run: allocations_since_gc = {}\",\n        output.allocations_since_gc\n    );\n\n    // Verify that GC collected most cycles.\n    // If GC failed to collect cycles, heap_count would be >> 200k.\n    assert!(\n        output.heap_count < 20,\n        \"GC should collect most unreachable list cycles: {} heap objects (expected < 20)\",\n        output.heap_count\n    );\n\n    // Verify expected ref counts\n    // `a` is the last self-referencing list (refcount 2: variable + self-reference)\n    // `result` is a simple list (refcount 1: just the variable)\n    assert_eq!(\n        output.counts.get(\"a\"),\n        Some(&2),\n        \"self-referencing list should have refcount 2\"\n    );\n    assert_eq!(\n        output.counts.get(\"result\"),\n        Some(&1),\n        \"result list should have refcount 1\"\n    );\n}\n\n/// Test that allocation limits return an error.\n#[test]\nfn allocation_limit_exceeded() {\n    // Use multi-character strings to ensure heap allocation (single ASCII chars are interned)\n    let code = r\"\nresult = []\nfor i in range(100, 115):\n    result.append(str(i))\nresult\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_allocations(4);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    // Should fail due to allocation limit\n    assert!(result.is_err(), \"should exceed allocation limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"allocation limit exceeded\")),\n        \"expected allocation limit error, got: {exc}\"\n    );\n}\n\n#[test]\nfn allocation_limit_not_exceeded() {\n    // Single-digit strings are interned (no allocation), so this uses minimal heap\n    let code = r\"\nresult = []\nfor i in range(9):\n    result.append(str(i))\nresult\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Allocations: list (1) + range (1) + iterator (1) = 3\n    // Note: str(0)...str(8) are single ASCII chars, so they use pre-interned strings\n    let limits = ResourceLimits::new().max_allocations(5);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    // Should succeed\n    assert!(result.is_ok(), \"should not exceed allocation limit\");\n}\n\n#[test]\nfn time_limit_exceeded() {\n    // Create a long-running loop using for + range (while isn't implemented yet)\n    // Use a very large range to ensure it runs long enough to hit the time limit\n    let code = r\"\nx = 0\nfor i in range(100000000):\n    x = x + 1\nx\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a short time limit\n    let limits = ResourceLimits::new().max_duration(Duration::from_millis(50));\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    // Should fail due to time limit\n    assert!(result.is_err(), \"should exceed time limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::TimeoutError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"time limit exceeded\")),\n        \"expected time limit error, got: {exc}\"\n    );\n}\n\n#[test]\nfn time_limit_not_exceeded() {\n    // Simple code that runs quickly\n    let code = \"x = 1 + 2\\nx\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a generous time limit\n    let limits = ResourceLimits::new().max_duration(Duration::from_secs(5));\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    // Should succeed\n    assert!(result.is_ok(), \"should not exceed time limit\");\n}\n\n/// Test that memory limits return an error.\n#[test]\nfn memory_limit_exceeded() {\n    // Create code that builds up memory using lists\n    // Each iteration creates a new list that gets appended\n    let code = r\"\nresult = []\nfor i in range(100):\n    result.append([1, 2, 3, 4, 5])\nresult\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a very low memory limit (100 bytes) to trigger on nested list allocation\n    let limits = ResourceLimits::new().max_memory(100);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    // Should fail due to memory limit\n    assert!(result.is_err(), \"should exceed memory limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n#[test]\nfn combined_limits() {\n    // Test multiple limits together\n    let code = \"x = 1 + 2\\nx\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new()\n        .max_allocations(1000)\n        .max_duration(Duration::from_secs(5))\n        .max_memory(1024 * 1024);\n\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n    assert!(result.is_ok(), \"should succeed with generous limits\");\n}\n\n#[test]\nfn run_without_limits_succeeds() {\n    // Verify that run() still works (no limits)\n    let code = r\"\nresult = []\nfor i in range(100):\n    result.append(str(i))\nlen(result)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Standard run should succeed\n    let result = ex.run_no_limits(vec![]);\n    assert!(result.is_ok(), \"standard run should succeed\");\n}\n\n#[test]\nfn gc_interval_triggers_collection() {\n    // This test verifies that GC can run without crashing\n    // We can't easily verify that GC actually collected anything without\n    // adding more introspection, but we can verify it runs\n    let code = r\"\nresult = []\nfor i in range(100):\n    temp = [1, 2, 3]\n    result.append(i)\nlen(result)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set GC to run every 10 allocations\n    let limits = ResourceLimits::new().gc_interval(10);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"should succeed with GC enabled\");\n}\n\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn executor_iter_resource_limit_on_resume() {\n    // Test that resource limits are enforced across function calls\n    // First function call succeeds, but resumed execution exceeds limit\n\n    // f-string to create multi-char strings (not interned)\n    let code = \"foo(1)\\nx = []\\nfor i in range(10):\\n    x.append(f'x{i}')\\nlen(x)\";\n    let run = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // First function call should succeed with generous limit\n    let limits = ResourceLimits::new().max_allocations(5);\n    let progress = run\n        .start(vec![], LimitedTracker::new(limits), PrintWriter::Stdout)\n        .unwrap();\n    let call = resolve_name_lookups(progress)\n        .unwrap()\n        .into_function_call()\n        .expect(\"function call\");\n    assert_eq!(call.function_name, \"foo\");\n    assert_eq!(call.args, vec![MontyObject::Int(1)]);\n\n    // Resume - should fail due to allocation limit during the for loop\n    let result = call.resume(MontyObject::None, PrintWriter::Stdout);\n    assert!(result.is_err(), \"should exceed allocation limit on resume\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"allocation limit exceeded\")),\n        \"expected allocation limit error, got: {exc}\"\n    );\n}\n\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn executor_iter_resource_limit_before_function_call() {\n    // Test that resource limits are enforced before first function call\n\n    // f-string to create multi-char strings (not interned)\n    let code = \"x = []\\nfor i in range(10):\\n    x.append(f'x{i}')\\nfoo(len(x))\\n42\";\n    let run = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Should fail before reaching the function call\n    let limits = ResourceLimits::new().max_allocations(3);\n    let result = run.start(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should exceed allocation limit before function call\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"allocation limit exceeded\")),\n        \"expected allocation limit error, got: {exc}\"\n    );\n}\n\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn char_f_string_not_allocated() {\n    // Single character f-string interned not not allocated\n\n    let code = \"x = []\\nfor i in range(10):\\n    x.append(f'{i}')\";\n    let run = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_allocations(4);\n    run.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout)\n        .unwrap();\n}\n\n#[test]\nfn executor_iter_resource_limit_multiple_function_calls() {\n    // Test resource limits across multiple function calls\n    let code = \"foo(1)\\nbar(2)\\nbaz(3)\\n4\";\n    let run = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Very tight allocation limit - should still work for simple function calls\n    let limits = ResourceLimits::new().max_allocations(100);\n\n    let progress = run\n        .start(vec![], LimitedTracker::new(limits), PrintWriter::Stdout)\n        .unwrap();\n    let call = resolve_name_lookups(progress)\n        .unwrap()\n        .into_function_call()\n        .expect(\"first call\");\n    assert_eq!(call.function_name, \"foo\");\n    assert_eq!(call.args, vec![MontyObject::Int(1)]);\n\n    let progress = call.resume(MontyObject::None, PrintWriter::Stdout).unwrap();\n    let call = resolve_name_lookups(progress)\n        .unwrap()\n        .into_function_call()\n        .expect(\"second call\");\n    assert_eq!(call.function_name, \"bar\");\n    assert_eq!(call.args, vec![MontyObject::Int(2)]);\n\n    let progress = call.resume(MontyObject::None, PrintWriter::Stdout).unwrap();\n    let call = resolve_name_lookups(progress)\n        .unwrap()\n        .into_function_call()\n        .expect(\"third call\");\n    assert_eq!(call.function_name, \"baz\");\n    assert_eq!(call.args, vec![MontyObject::Int(3)]);\n\n    let result = call\n        .resume(MontyObject::None, PrintWriter::Stdout)\n        .unwrap()\n        .into_complete()\n        .expect(\"complete\");\n    assert_eq!(result, MontyObject::Int(4));\n}\n\n/// Test that deep recursion triggers memory limit due to namespace tracking.\n///\n/// Function call namespaces (local variables) are tracked by ResourceTracker.\n/// Each recursive call creates a new namespace, which should count against\n/// the memory limit.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn recursion_respects_memory_limit() {\n    // Recursive function that creates stack frames with local variables\n    let code = r\"\ndef recurse(n):\n    x = 1\n    if n > 0:\n        return recurse(n - 1)\n    return 0\nrecurse(1000)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Very tight memory limit - should fail due to namespace memory\n    // Each frame needs at least namespace_size * size_of::<Value>() bytes\n    let limits = ResourceLimits::new().max_memory(1000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should exceed memory limit from recursion\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that recursion depth limit returns an error.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn recursion_depth_limit_exceeded() {\n    let code = r\"\ndef recurse(n):\n    if n > 0:\n        return recurse(n - 1)\n    return 0\nrecurse(100)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set recursion limit to 10\n    let limits = ResourceLimits::new().max_recursion_depth(Some(10));\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should exceed recursion depth limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::RecursionError);\n    assert!(\n        exc.message()\n            .is_some_and(|m| m.contains(\"maximum recursion depth exceeded\")),\n        \"expected recursion depth error, got: {exc}\"\n    );\n}\n\n#[test]\nfn recursion_depth_limit_not_exceeded() {\n    let code = r\"\ndef recurse(n):\n    if n > 0:\n        return recurse(n - 1)\n    return 0\nrecurse(5)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set recursion limit to 10 - should succeed with 5 levels\n    let limits = ResourceLimits::new().max_recursion_depth(Some(10));\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"should not exceed recursion depth limit\");\n}\n\n// === BigInt large result pre-check tests ===\n// These tests verify that operations that would produce very large BigInt results\n// are rejected before the computation begins, preventing DoS attacks.\n\n/// Test that large pow operations are rejected by memory limits.\n#[test]\nfn bigint_pow_memory_limit() {\n    // 2 ** 10_000_000 would produce ~1.25MB result\n    let code = \"2 ** 10000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a 1MB memory limit - should fail before computing\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large pow should exceed memory limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that pow with huge exponents is rejected even when the size estimate overflows u64.\n///\n/// This catches a bug where `estimate_pow_bytes` returned `None` on u64 overflow,\n/// and the `if let Some(estimated)` pattern silently skipped the check.\n#[test]\nfn pow_overflowing_estimate_rejected() {\n    // base ~63 bits, exp ~62 bits: estimated result bits = 63 * 3962939411543162624 overflows u64\n    let code = \"-7234189268083315611 ** 3962939411543162624\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"pow with overflowing estimate should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that pow with a large base and moderate exponent is rejected by memory limits.\n///\n/// `-7234408281351689115 ** 65327` has a 63-bit base, so the result is ~63*65327 ≈ 4M bits ≈ 514KB.\n/// With a 100KB memory limit the pre-check should reject this before computing.\n#[test]\nfn pow_large_base_moderate_exp_rejected() {\n    let code = \"-7234408281351689115 ** 65327\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large pow should exceed memory limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that the 4× safety multiplier for pow intermediate allocations catches\n/// cases where the final result fits but repeated-squaring intermediates don't.\n///\n/// `2 ** 500000`: final result = 2 * 500000 bits = 125KB. Without multiplier this\n/// passes a 200KB limit. With 4× multiplier: 500KB > 200KB → rejected.\n#[test]\nfn pow_intermediate_allocation_multiplier() {\n    let code = \"2 ** 500000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // 200KB limit: final result (125KB) fits, but 4× estimate (500KB) exceeds it\n    let limits = ResourceLimits::new().max_memory(200_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(\n        result.is_err(),\n        \"pow should be rejected due to intermediate allocation overhead\"\n    );\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    // 2 bits * 500000 = 125KB final, × 4 = 500072 bytes (includes base memory offset)\n    assert_eq!(\n        exc.message(),\n        Some(\"memory limit exceeded: 500072 bytes > 200000 bytes\")\n    );\n}\n\n/// Test that pow still succeeds when the 4× estimate is within the limit.\n///\n/// `2 ** 100000`: final result = 2 * 100000 bits ≈ 25KB. With 4× multiplier: ~100KB.\n/// A 1MB limit should comfortably allow this.\n#[test]\nfn pow_within_limit_with_multiplier() {\n    let code = \"x = 2 ** 100000\\nx > 0\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"pow with 4× estimate under limit should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test the exact fuzzer OOM pattern: right-associative chained exponentiation.\n///\n/// `3 ** 3661666` is the first sub-expression of the fuzzer input\n/// `1666**3**366**3**3661666`. Since `**` is right-associative, `3**3661666`\n/// is computed first. Base 3 has 2 bits, so: 2 * 3661666 = 7323332 bits ≈ 915KB.\n/// With 4× multiplier: 3660KB > 1MB fuzz limit → rejected.\n#[test]\nfn pow_fuzzer_oom_chained_exponentiation() {\n    // This is the subexpression that caused the fuzzer OOM\n    let code = \"3 ** 3661666\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // 1MB limit (matching the fuzzer's resource limit)\n    let limits = ResourceLimits::new().max_memory(1_024 * 1_024);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(\n        result.is_err(),\n        \"fuzzer OOM pattern should be rejected by 4× multiplier\"\n    );\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    // 2 bits * 3661666 = 915KB final, × 4 = 3661740 bytes\n    assert_eq!(\n        exc.message(),\n        Some(\"memory limit exceeded: 3661740 bytes > 1048576 bytes\")\n    );\n}\n\n/// Test the full fuzzer input that originally caused OOM.\n///\n/// The input `1666**3**366**3**3661666` should be rejected before any large\n/// intermediate allocation occurs.\n#[test]\nfn pow_fuzzer_oom_full_input() {\n    let code = \"1666**3**366**3**3661666\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(1_024 * 1_024);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"full fuzzer OOM input should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    // 3**3661666 is evaluated first (right-associative). Base 3 = 2 bits,\n    // so estimate = 2 * 3661666 bits = 915KB. With 4× multiplier: 3661740 bytes > 1MB.\n    assert_eq!(\n        exc.message(),\n        Some(\"memory limit exceeded: 3661740 bytes > 1048576 bytes\")\n    );\n}\n\n/// Test that large left shift operations are rejected by memory limits.\n#[test]\nfn bigint_lshift_memory_limit() {\n    // 1 << 10_000_000 would produce ~1.25MB result\n    let code = \"1 << 10000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a 1MB memory limit - should fail before computing\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large lshift should exceed memory limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that large multiplication operations are rejected by memory limits.\n#[test]\nfn bigint_mult_memory_limit() {\n    // (2**4_000_000) * (2**4_000_000) would produce ~1MB result\n    let code = \"big = 2 ** 4000000\\nbig * big\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a 1MB memory limit - should fail before computing the multiplication\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large mult should exceed memory limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that small BigInt operations succeed within memory limits.\n#[test]\nfn bigint_small_operations_within_limit() {\n    // 2 ** 1000 produces ~125 bytes - well under limit\n    let code = \"x = 2 ** 1000\\ny = 1 << 1000\\nz = x * 2\\nx > 0 and y > 0 and z > 0\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Set a 1MB memory limit - should succeed\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small BigInt operations should succeed within limit\");\n    let val = result.unwrap();\n    assert_eq!(val, MontyObject::Bool(true));\n}\n\n/// Test that edge cases (0, 1, -1) with huge exponents succeed even with limits.\n/// These produce constant-size results regardless of exponent.\n#[test]\nfn bigint_edge_cases_always_succeed() {\n    // Test each edge case individually to minimize other allocations\n    // These edge cases produce constant-size results regardless of exponent:\n    // - 0 ** huge = 0\n    // - 1 ** huge = 1\n    // - (-1) ** huge = 1 or -1\n    // - 0 << huge = 0\n\n    // 1MB limit would reject 2**10000000 (~1.25MB) but allows edge cases\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n\n    // 0 ** huge = 0\n    let code = \"0 ** 10000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run(vec![], LimitedTracker::new(limits.clone()), PrintWriter::Stdout);\n    assert!(result.is_ok(), \"0 ** huge should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Int(0));\n\n    // 1 ** huge = 1\n    let code = \"1 ** 10000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run(vec![], LimitedTracker::new(limits.clone()), PrintWriter::Stdout);\n    assert!(result.is_ok(), \"1 ** huge should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Int(1));\n\n    // (-1) ** huge_even = 1\n    let code = \"(-1) ** 10000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run(vec![], LimitedTracker::new(limits.clone()), PrintWriter::Stdout);\n    assert!(result.is_ok(), \"(-1) ** huge_even should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Int(1));\n\n    // (-1) ** huge_odd = -1\n    let code = \"(-1) ** 10000001\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run(vec![], LimitedTracker::new(limits.clone()), PrintWriter::Stdout);\n    assert!(result.is_ok(), \"(-1) ** huge_odd should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Int(-1));\n\n    // 0 << huge = 0\n    let code = \"0 << 10000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n    assert!(result.is_ok(), \"0 << huge should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Int(0));\n}\n\n/// Test that pow() builtin also respects memory limits.\n#[test]\nfn bigint_builtin_pow_memory_limit() {\n    let code = \"pow(2, 10000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(1_000_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"builtin pow should respect memory limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that large BigInt operations are rejected BEFORE allocation via check_large_result.\n///\n/// The pre-allocation size check estimates result size and rejects operations that would\n/// exceed the memory limit before any memory is actually consumed.\n#[test]\nfn bigint_rejected_before_allocation() {\n    // 2**1000000: base 2 has 2 bits, so estimate = 2 * 1000000 bits = 250KB\n    // With 4× safety multiplier for intermediate allocations = 1000KB\n    // Set limit to 100KB - the pre-check should reject before allocating\n    let code = \"2 ** 1000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should be rejected before allocation\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert_eq!(\n        exc.message(),\n        Some(\"memory limit exceeded: 1000072 bytes > 100000 bytes\")\n    );\n}\n\n// === String/Bytes large result pre-check tests ===\n// These tests verify that string/bytes multiplication operations that would produce\n// very large results are rejected before the computation begins.\n\n/// Test that large string multiplication is rejected before allocation.\n#[test]\nfn string_mult_memory_limit() {\n    // 'x' * 1000000 = 1MB string\n    let code = \"'x' * 1000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large string mult should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that large bytes multiplication is rejected before allocation.\n#[test]\nfn bytes_mult_memory_limit() {\n    // b'x' * 1000000 = 1MB bytes\n    let code = \"b'x' * 1000000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large bytes mult should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that small string multiplication works within limits.\n#[test]\nfn string_mult_within_limit() {\n    // 'abc' * 100 = 300 bytes, well within 100KB limit\n    let code = \"'abc' * 100 == 'abc' * 100\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small string mult should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that small bytes multiplication works within limits.\n#[test]\nfn bytes_mult_within_limit() {\n    // b'abc' * 100 = 300 bytes, well within 100KB limit\n    let code = \"b'abc' * 100 == b'abc' * 100\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small bytes mult should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that string multiplication is rejected before allocation via check_large_result.\n#[test]\nfn string_mult_rejected_before_allocation() {\n    // 'x' * 200000 = 200KB string\n    // Set limit to 100KB - the pre-check should reject before allocating\n    let code = \"'x' * 200000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"should be rejected before allocation\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    // The exact size may include some overhead, but should be around 200KB\n    assert!(\n        exc.message()\n            .is_some_and(|m| m.contains(\"memory limit exceeded\") && m.contains(\"> 100000 bytes\")),\n        \"expected memory limit error with ~200KB size, got: {:?}\",\n        exc.message()\n    );\n}\n\n/// Test that large list multiplication is rejected before allocation.\n#[test]\nfn list_mult_memory_limit() {\n    // [1] * 10000 = 10,000 Values = ~160KB (at 16 bytes per Value)\n    let code = \"[1] * 10000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large list mult should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that large tuple multiplication is rejected before allocation.\n#[test]\nfn tuple_mult_memory_limit() {\n    // (1,) * 10000 = 10,000 Values = ~160KB (at 16 bytes per Value)\n    let code = \"(1,) * 10000\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"large tuple mult should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that small list multiplication works within limits.\n#[test]\nfn list_mult_within_limit() {\n    // [1, 2, 3] * 20 = 60 Values, well within 100KB limit\n    let code = \"[1, 2, 3] * 20 == [1, 2, 3] * 20\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small list mult should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that `int * bytes` (int on left) is also rejected by the pre-check.\n///\n/// This catches a bug where interned bytes/strings bypassed the `mult_sequence`\n/// pre-check because `py_mult` handled `InternBytes * Int` inline without\n/// checking resource limits.\n#[test]\nfn int_times_bytes_memory_limit() {\n    // int on left side: 1000000 * b'x' = 1MB\n    let code = \"1000000 * b'x'\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"int * bytes should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that `int * str` (int on left) is also rejected by the pre-check.\n#[test]\nfn int_times_string_memory_limit() {\n    // int on left side: 1000000 * 'x' = 1MB\n    let code = \"1000000 * 'x'\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000); // 100KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"int * str should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that `bigint * bytes` (LongInt on left) is rejected by the pre-check.\n#[test]\nfn longint_times_bytes_memory_limit() {\n    // i64::MAX + 1 = 9223372036854775808, which is a LongInt but fits in usize on 64-bit.\n    // Multiplied by 1-byte bytes literal, this would be ~9.2 exabytes.\n    let code = \"9223372036854775808 * b'x'\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bigint * bytes should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that `bigint * str` (LongInt on left) is rejected by the pre-check.\n#[test]\nfn longint_times_string_memory_limit() {\n    // i64::MAX + 1 = 9223372036854775808, which is a LongInt but fits in usize on 64-bit.\n    let code = \"9223372036854775808 * 'x'\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bigint * str should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that small tuple multiplication works within limits.\n#[test]\nfn tuple_mult_within_limit() {\n    // (1, 2, 3) * 20 = 60 Values, well within 100KB limit\n    let code = \"(1, 2, 3) * 20 == (1, 2, 3) * 20\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small tuple mult should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n// === Timeout enforcement in builtin iteration loops ===\n// These tests verify that `max_duration_secs` is enforced inside Rust-side loops\n// within builtin functions. Previously, builtins like sum(), sorted(), min(), max()\n// ran Rust loops entirely within a single bytecode instruction, bypassing the VM's\n// per-instruction timeout check. The fix adds `heap.check_time()` calls inside\n// `MontyIter::for_next()` and other non-iterator loops.\n\n/// Helper: runs code with a short time limit and asserts it produces a TimeoutError promptly.\nfn assert_timeout_in_builtin(code: &str, label: &str) {\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_duration(Duration::from_millis(100));\n    let start = std::time::Instant::now();\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n    let elapsed = start.elapsed();\n\n    assert!(result.is_err(), \"{label}: should exceed time limit\");\n    let exc = result.unwrap_err();\n    assert_eq!(\n        exc.exc_type(),\n        ExcType::TimeoutError,\n        \"{label}: expected TimeoutError, got: {exc}\"\n    );\n    assert!(\n        elapsed < Duration::from_secs(2),\n        \"{label}: should terminate promptly, took {elapsed:?}\"\n    );\n}\n\n/// Test that `sum(range(huge))` respects the time limit.\n///\n/// `sum()` iterates via `for_next()` which now calls `heap.check_time()`.\n#[test]\nfn timeout_in_sum_builtin() {\n    assert_timeout_in_builtin(\"sum(range(10**18))\", \"sum(range(10**18))\");\n}\n\n/// Test that `list(range(huge))` respects the time limit.\n///\n/// The `list()` constructor collects via `MontyIter::collect()` -> `for_next()`.\n#[test]\nfn timeout_in_list_constructor() {\n    assert_timeout_in_builtin(\"list(range(10**18))\", \"list(range(10**18))\");\n}\n\n/// Test that `sorted(range(huge))` respects the time limit.\n///\n/// `sorted()` first collects items via `for_next()`, then sorts. The collection\n/// phase alone should trigger the timeout for very large ranges.\n#[test]\nfn timeout_in_sorted_builtin() {\n    assert_timeout_in_builtin(\"sorted(range(10**18))\", \"sorted(range(10**18))\");\n}\n\n/// Test that `min(range(huge))` respects the time limit.\n///\n/// `min()` with a single iterable argument iterates via `for_next()`.\n#[test]\nfn timeout_in_min_builtin() {\n    assert_timeout_in_builtin(\"min(range(10**18))\", \"min(range(10**18))\");\n}\n\n/// Test that `max(range(huge))` respects the time limit.\n///\n/// `max()` with a single iterable argument iterates via `for_next()`.\n#[test]\nfn timeout_in_max_builtin() {\n    assert_timeout_in_builtin(\"max(range(10**18))\", \"max(range(10**18))\");\n}\n\n/// Test that `all(range(huge))` respects the time limit.\n///\n/// `all()` iterates via `for_next()` and only short-circuits on falsy values.\n/// `range(1, 10**18)` produces only truthy values so it keeps iterating.\n#[test]\nfn timeout_in_all_builtin() {\n    assert_timeout_in_builtin(\"all(range(1, 10**18))\", \"all(range(1, 10**18))\");\n}\n\n/// Test that `enumerate(range(huge))` iteration respects the time limit.\n///\n/// `enumerate()` creates tuples on each iteration via `for_next()`.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_any_builtin() {\n    // range(0, 1) repeated via a for loop calling any on each chunk isn't ideal,\n    // but we can test with a large range starting from 0 where only first element is falsy\n    // Actually, any(range(10**18)) will return True immediately because range starts at 0\n    // which is falsy, but 1 is truthy. So any() returns True after checking 0, 1.\n    // Instead, we need a different approach - just use the for_next timeout via enumerate.\n    assert_timeout_in_builtin(\"list(enumerate(range(10**18)))\", \"enumerate(range(10**18))\");\n}\n\n/// Test that `tuple(range(huge))` respects the time limit.\n///\n/// The `tuple()` constructor collects via `MontyIter::collect()` -> `for_next()`.\n#[test]\nfn timeout_in_tuple_constructor() {\n    assert_timeout_in_builtin(\"tuple(range(10**18))\", \"tuple(range(10**18))\");\n}\n\n/// Test that `' '.join(...)` iteration respects the time limit.\n///\n/// `str.join()` collects items from the iterable via `for_next()`.\n#[test]\nfn timeout_in_str_join() {\n    assert_timeout_in_builtin(\"' '.join(str(i) for i in range(10**18))\", \"str.join with generator\");\n}\n\n/// Test that the insertion sort inner loop in `sorted()` respects the time limit.\n///\n/// Uses reverse-sorted data to trigger worst-case O(n^2) insertion sort behavior.\n/// The sort comparison loop has an explicit `heap.check_time()` call.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_sorted_comparison_loop() {\n    // Build a reverse-sorted list, then sort it. Insertion sort on reverse-sorted\n    // data is O(n^2).\n    let code = r\"\nx = list(range(10**6, 0, -1))\nsorted(x)\n\";\n    assert_timeout_in_builtin(code, \"sorted(reversed list)\");\n}\n\n/// Test that `[1] * 10_000_000` (list repetition) respects the time limit.\n///\n/// The `mult_sequence()` copy loop now calls `heap.check_time()` on each\n/// repetition to prevent large sequence multiplications from bypassing timeout.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_list_repetition() {\n    assert_timeout_in_builtin(\"[1, 2, 3] * 10_000_000\", \"list repetition\");\n}\n\n/// Test that `(1,) * 10_000_000` (tuple repetition) respects the time limit.\n///\n/// Same as list repetition but for tuples — both paths in `mult_sequence()`\n/// now check the time limit.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_tuple_repetition() {\n    assert_timeout_in_builtin(\"(1, 2, 3) * 10_000_000\", \"tuple repetition\");\n}\n\n/// Test that comparing two large equal lists respects the time limit.\n///\n/// `List::py_eq()` iterates element-wise comparing pairs. With large equal lists,\n/// it must compare every element before returning True.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_list_equality() {\n    let code = r\"\na = list(range(10_000_000))\nb = list(range(10_000_000))\na == b\n\";\n    assert_timeout_in_builtin(code, \"list equality\");\n}\n\n/// Test that comparing two large equal dicts respects the time limit.\n///\n/// `Dict::py_eq()` iterates all entries checking keys and values. With large equal\n/// dicts, it must check every entry before returning True.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_dict_equality() {\n    let code = r\"\na = {i: i for i in range(10_000_000)}\nb = {i: i for i in range(10_000_000)}\na == b\n\";\n    assert_timeout_in_builtin(code, \"dict equality\");\n}\n\n/// Test that `str.splitlines()` on a large string respects the time limit.\n///\n/// `str_splitlines()` scans the entire string for line endings in a while loop\n/// that now calls `heap.check_time()` on each iteration.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_str_splitlines() {\n    let code = r\"\ns = 'a\\n' * 5_000_000\ns.splitlines()\n\";\n    assert_timeout_in_builtin(code, \"str.splitlines()\");\n}\n\n/// Test that `bytes.splitlines()` on large bytes respects the time limit.\n///\n/// `bytes_splitlines()` scans bytes for line endings and now checks the time limit.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_in_bytes_splitlines() {\n    let code = r\"\ns = b'a\\n' * 5_000_000\ns.splitlines()\n\";\n    assert_timeout_in_builtin(code, \"bytes.splitlines()\");\n}\n\n// === Timeout truncation in repr ===\n// These tests verify that `repr()` on large containers respects the time limit\n// and terminates promptly instead of hanging indefinitely. The repr methods\n// (`repr_sequence_fmt`, `Dict::py_repr_fmt`, `SetInner::repr_fmt`) call\n// `heap.check_time()` on each iteration and write `...[timeout]` when the\n// time limit is exceeded, returning normally instead of propagating an error.\n//\n// Each test uses the external function \"interrupt\" pattern: the large object is\n// built with NO time limit, then execution pauses at `interrupt()`. A short time\n// limit is set before resuming, so only the `repr()` call is timed.\n\n/// Helper: builds a large object without time limit, then runs `repr()` on it\n/// with a short time limit and asserts it produces a TimeoutError promptly.\n///\n/// The code must call `interrupt()` between object construction and `repr()`.\nfn assert_repr_timeout(code: &str, label: &str) {\n    let run = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    // Phase 1: build the large object with no time limit\n    let limits = ResourceLimits::new();\n    let progress = run\n        .start(vec![], LimitedTracker::new(limits), PrintWriter::Stdout)\n        .unwrap();\n    let mut call = resolve_name_lookups(progress)\n        .unwrap()\n        .into_function_call()\n        .expect(\"interrupt call\");\n    assert_eq!(call.function_name, \"interrupt\");\n\n    // Phase 2: set a short time limit and resume — repr() should timeout\n    call.tracker_mut().set_max_duration(Duration::from_millis(10));\n\n    let start = Instant::now();\n    let result = call.resume(MontyObject::None, PrintWriter::Stdout);\n    let elapsed = start.elapsed();\n\n    let exc = result.unwrap_err();\n    assert_eq!(\n        exc.exc_type(),\n        ExcType::TimeoutError,\n        \"{label}: expected TimeoutError, got: {exc}\"\n    );\n    let msg = exc.message().unwrap();\n    assert!(msg.starts_with(\"time limit exceeded:\"));\n    assert!(msg.ends_with(\"ms > 10ms\"));\n    assert!(\n        elapsed < Duration::from_millis(200),\n        \"{label}: should terminate promptly, took {elapsed:?}\"\n    );\n}\n\n/// Test that `repr(large_list)` respects the time limit.\n///\n/// Uses a list of 100K short strings so that repr formatting is slow enough\n/// to trigger the timeout.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_truncation_in_list_repr() {\n    let code = r\"\nx = ['abcdefghij'] * 100_000\ninterrupt()\nrepr(x)\n\";\n    assert_repr_timeout(code, \"list repr\");\n}\n\n/// Test that `repr(large_dict)` respects the time limit.\n///\n/// Uses a dict with 100K entries where values are short strings,\n/// making repr formatting slow enough to trigger the timeout.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_truncation_in_dict_repr() {\n    let code = r\"\nx = {i: 'abcdefghij' for i in range(100_000)}\ninterrupt()\nrepr(x)\n\";\n    assert_repr_timeout(code, \"dict repr\");\n}\n\n/// Test that `repr(large_set)` respects the time limit.\n///\n/// Uses a set of 100K unique strings so that repr formatting is slow enough\n/// to trigger the timeout.\n#[test]\n#[cfg_attr(\n    feature = \"ref-count-panic\",\n    ignore = \"resource exhaustion doesn't guarantee heap state consistency\"\n)]\nfn timeout_truncation_in_set_repr() {\n    let code = r\"\nx = {str(i) for i in range(100_000)}\ninterrupt()\nrepr(x)\n\";\n    assert_repr_timeout(code, \"set repr\");\n}\n\n/// Test that `str.replace` with amplification is rejected before allocation.\n///\n/// `'a' * 1000` is 1KB (within limit), but replacing each 'a' with a 1KB string\n/// produces a 1MB result. The pre-check should reject this before `String::replace()`\n/// allocates the result on the Rust heap.\n#[test]\nfn str_replace_amplification_memory_limit() {\n    let code = r\"\ns = 'a' * 1000\ns.replace('a', 'b' * 1000)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000); // 500KB limit\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"str.replace amplification should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that small `str.replace` works within limits.\n#[test]\nfn str_replace_within_limit() {\n    let code = \"'hello world'.replace('world', 'rust') == 'hello rust'\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small str.replace should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that `bytes.replace` with amplification is rejected before allocation.\n#[test]\nfn bytes_replace_amplification_memory_limit() {\n    let code = r\"\ns = b'a' * 1000\ns.replace(b'a', b'b' * 1000)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bytes.replace amplification should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that `str.replace` with empty pattern amplification is rejected.\n///\n/// Empty pattern inserts `new` before each char and after the last, so\n/// result size = input_len * (new_len + 1).\n#[test]\nfn str_replace_empty_pattern_memory_limit() {\n    let code = r\"\ns = 'a' * 500\ns.replace('', 'x' * 1000)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(200_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(\n        result.is_err(),\n        \"str.replace with empty pattern amplification should be rejected\"\n    );\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `str.ljust` with huge width is rejected before allocation.\n///\n/// Without the pre-check, `String::with_capacity(width)` would allocate\n/// directly on the Rust heap, bypassing the memory tracker entirely.\n#[test]\nfn str_ljust_memory_limit() {\n    let code = \"'x'.ljust(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"str.ljust with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that `str.rjust` with huge width is rejected before allocation.\n#[test]\nfn str_rjust_memory_limit() {\n    let code = \"'x'.rjust(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"str.rjust with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `str.center` with huge width is rejected before allocation.\n#[test]\nfn str_center_memory_limit() {\n    let code = \"'x'.center(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"str.center with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `str.zfill` with huge width is rejected before allocation.\n#[test]\nfn str_zfill_memory_limit() {\n    let code = \"'42'.zfill(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"str.zfill with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that small padding operations work within limits.\n#[test]\nfn str_padding_within_limit() {\n    let code = \"'hi'.ljust(10) == 'hi        '\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small padding should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that `bytes.ljust` with huge width is rejected before allocation.\n#[test]\nfn bytes_ljust_memory_limit() {\n    let code = \"b'x'.ljust(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bytes.ljust with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `bytes.rjust` with huge width is rejected before allocation.\n#[test]\nfn bytes_rjust_memory_limit() {\n    let code = \"b'x'.rjust(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bytes.rjust with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `bytes.center` with huge width is rejected before allocation.\n#[test]\nfn bytes_center_memory_limit() {\n    let code = \"b'x'.center(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bytes.center with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `bytes.zfill` with huge width is rejected before allocation.\n#[test]\nfn bytes_zfill_memory_limit() {\n    let code = \"b'42'.zfill(2000000)\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"bytes.zfill with huge width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that f-string formatting with huge width is rejected before allocation.\n#[test]\nfn fstring_dynamic_width_memory_limit() {\n    // Dynamic format spec via f-string nesting: {w} produces a runtime-parsed spec\n    let code = \"w = 2000000\\nf\\\"{'x':>{w}}\\\"\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"f-string with huge dynamic width should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n// === re.sub() memory tracking tests ===\n// These tests verify that the single-pass replacement loop in `re.sub()` tracks\n// the running output size and bails out when the resource limit is exceeded.\n\n/// Test that `re.sub` with every-char pattern amplification is rejected.\n///\n/// Pattern 'a' matches every character in 'aaa...'. Each replacement expands\n/// 1 byte → 1000 bytes, so the output grows to ~1MB which exceeds the 500KB limit.\n/// The inline loop catches this after a few hundred matches.\n#[test]\nfn re_sub_amplification_memory_limit() {\n    let code = r\"\nimport re\ns = 'a' * 1000\nre.sub('a', 'b' * 1000, s)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"re.sub amplification should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"memory limit exceeded\")),\n        \"expected memory limit error, got: {exc}\"\n    );\n}\n\n/// Test that `re.sub` with empty pattern amplification is rejected.\n///\n/// Empty pattern matches N+1 times for N-char input (between and around every\n/// character). Each match inserts 1000 bytes, so 501 matches × 1000 ≈ 500KB\n/// which exceeds the 200KB limit.\n#[test]\nfn re_sub_empty_pattern_amplification_memory_limit() {\n    let code = r\"\nimport re\ns = 'a' * 500\nre.sub('', 'x' * 1000, s)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(200_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(\n        result.is_err(),\n        \"re.sub with empty pattern amplification should be rejected\"\n    );\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `pattern.sub` (compiled pattern method) is also rejected.\n#[test]\nfn re_pattern_sub_amplification_memory_limit() {\n    let code = r\"\nimport re\np = re.compile('a')\ns = 'a' * 1000\np.sub('b' * 1000, s)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"pattern.sub amplification should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n/// Test that `re.sub` raises `re.PatternError` when the regex engine hits its backtracking limit.\n///\n/// The pattern `(a+)+\\1b` forces `fancy_regex` into its backtracking VM (due to the\n/// backreference `\\1`). With enough `a`s followed by a non-matching character, the\n/// exponential blowup exceeds the engine's backtracking step limit (~1M steps).\n#[test]\nfn re_sub_backtracking_limit_raises_pattern_error() {\n    let code = r\"\nimport re\nre.sub('(a+)+\\\\1b', 'X', 'a' * 30 + 'c')\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"backtracking limit should raise an error\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::RePatternError);\n    assert!(\n        exc.message().is_some_and(|m| m.contains(\"backtrack\")),\n        \"expected backtracking error, got: {exc}\"\n    );\n}\n\n// --- Selective patterns: few matches in large text stay within limits ---\n\n/// Test that a selective pattern on large text passes.\n///\n/// The pattern `xxx` only matches 3 times (at positions 0, 3, 6 in the 9-char prefix),\n/// so the result is ~10000 - 9 + 300 = 10291 bytes — well within the 500KB limit.\n#[test]\nfn re_sub_selective_pattern_passes() {\n    // 'xxx' repeated 3 times at the start, rest is 'a's\n    let code = r\"\nimport re\ns = 'xxx' * 3 + 'a' * 9991\nresult = re.sub('xxx', 'y' * 100, s)\nlen(result)  == 9991 + 3 * 100\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(\n        result.is_ok(),\n        \"selective pattern with few matches should pass: {result:?}\"\n    );\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that a digit-matching pattern on mostly-text input passes.\n///\n/// Pattern `\\d+` matches only the 10-digit number, so the result is\n/// 990 + 200 = 1190 bytes — well within the 150KB limit.\n#[test]\nfn re_sub_digit_pattern_passes() {\n    let code = r\"\nimport re\ns = 'a' * 990 + '1234567890'\nresult = re.sub('\\d+', 'X' * 200, s)\nlen(result) == 990 + 200\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(150_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"digit pattern on mostly-text should pass: {result:?}\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that every-char amplification is still rejected even with a generic pattern.\n///\n/// Pattern `.` matches every character (10000 matches), each expanding 1 → 1000 bytes.\n/// The inline loop catches this after a few hundred matches once the running output\n/// size exceeds the 500KB limit.\n#[test]\nfn re_sub_every_char_amplification_rejected() {\n    let code = r\"\nimport re\ns = 'a' * 10000\nre.sub('.', 'b' * 1000, s)\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_err(), \"every-char pattern amplification should be rejected\");\n    let exc = result.unwrap_err();\n    assert_eq!(exc.exc_type(), ExcType::MemoryError);\n}\n\n// --- General re.sub tests ---\n\n/// Test that small `re.sub` works within limits.\n#[test]\nfn re_sub_within_limit() {\n    let code = r\"\nimport re\nre.sub('world', 'rust', 'hello world') == 'hello rust'\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(100_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"small re.sub should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n\n/// Test that `re.sub` with count parameter limits replacements correctly.\n///\n/// `count=5` caps replacements to 5, so the result is\n/// 995 unchanged bytes + 5 × 100 replacement bytes = 1495 bytes.\n#[test]\nfn re_sub_with_count_within_limit() {\n    let code = r\"\nimport re\nre.sub('a', 'b' * 100, 'a' * 1000, count=5) == 'b' * 500 + 'a' * 995\n\";\n    let ex = MontyRun::new(code.to_owned(), \"test.py\", vec![]).unwrap();\n\n    let limits = ResourceLimits::new().max_memory(500_000);\n    let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);\n\n    assert!(result.is_ok(), \"re.sub with small count should succeed\");\n    assert_eq!(result.unwrap(), MontyObject::Bool(true));\n}\n"
  },
  {
    "path": "crates/monty/tests/try_from.rs",
    "content": "use monty::MontyRun;\n\n/// Tests for successful TryFrom conversions from Python values to Rust types.\n///\n/// These tests validate that the `TryFrom` implementations on `MontyObject` correctly\n/// convert Python objects to their corresponding Rust types when the conversion\n/// is valid (e.g., Python int to Rust i64, Python str to Rust String).\n\n#[test]\nfn try_from_ok_int_to_i64() {\n    let ex = MontyRun::new(\"42\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: i64 = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, 42);\n}\n\n#[test]\nfn try_from_ok_zero_to_i64() {\n    let ex = MontyRun::new(\"0\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: i64 = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, 0);\n}\n\n#[test]\n#[expect(clippy::float_cmp)]\nfn try_from_ok_float_to_f64() {\n    let ex = MontyRun::new(\"2.5\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: f64 = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, 2.5);\n}\n\n#[test]\n#[expect(clippy::float_cmp)]\nfn try_from_ok_int_to_f64() {\n    let ex = MontyRun::new(\"42\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: f64 = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, 42.0);\n}\n\n#[test]\nfn try_from_ok_string_to_string() {\n    let ex = MontyRun::new(\"'hello'\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: String = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, \"hello\".to_string());\n}\n\n#[test]\nfn try_from_ok_empty_string_to_string() {\n    let ex = MontyRun::new(\"''\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: String = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, String::new());\n}\n\n#[test]\nfn try_from_ok_multiline_string_to_string() {\n    let ex = MontyRun::new(\"'hello\\\\nworld'\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: String = (&result).try_into().expect(\"conversion should succeed\");\n    assert_eq!(value, \"hello\\nworld\".to_string());\n}\n\n#[test]\nfn try_from_ok_bool_true_to_bool() {\n    let ex = MontyRun::new(\"True\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: bool = (&result).try_into().expect(\"conversion should succeed\");\n    assert!(value);\n}\n\n#[test]\nfn try_from_ok_bool_false_to_bool() {\n    let ex = MontyRun::new(\"False\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let value: bool = (&result).try_into().expect(\"conversion should succeed\");\n    assert!(!value);\n}\n\n/// Tests for failed TryFrom conversions from Python values to Rust types.\n///\n/// These tests validate that the `TryFrom` implementations correctly reject\n/// invalid conversions with appropriate error messages (e.g., trying to convert\n/// a Python str to a Rust i64).\n\n#[test]\nfn try_from_err_string_to_i64() {\n    let ex = MontyRun::new(\"'hello'\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<i64>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected int, got str\");\n}\n\n#[test]\nfn try_from_err_float_to_i64() {\n    let ex = MontyRun::new(\"2.5\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<i64>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected int, got float\");\n}\n\n#[test]\nfn try_from_err_none_to_i64() {\n    let ex = MontyRun::new(\"None\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<i64>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected int, got NoneType\");\n}\n\n#[test]\nfn try_from_err_list_to_i64() {\n    let ex = MontyRun::new(\"[1, 2, 3]\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<i64>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected int, got list\");\n}\n\n#[test]\nfn try_from_err_int_to_string() {\n    let ex = MontyRun::new(\"42\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<String>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected str, got int\");\n}\n\n#[test]\nfn try_from_err_none_to_string() {\n    let ex = MontyRun::new(\"None\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<String>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected str, got NoneType\");\n}\n\n#[test]\nfn try_from_err_list_to_string() {\n    let ex = MontyRun::new(\"[1, 2]\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<String>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected str, got list\");\n}\n\n#[test]\nfn try_from_err_int_to_bool() {\n    let ex = MontyRun::new(\"1\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<bool>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected bool, got int\");\n}\n\n#[test]\nfn try_from_err_string_to_bool() {\n    let ex = MontyRun::new(\"'true'\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<bool>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected bool, got str\");\n}\n\n#[test]\nfn try_from_err_none_to_bool() {\n    let ex = MontyRun::new(\"None\".to_owned(), \"test.py\", vec![]).unwrap();\n    let result = ex.run_no_limits(vec![]).unwrap();\n    let err = TryInto::<bool>::try_into(&result).expect_err(\"conversion should fail\");\n    assert_eq!(err.to_string(), \"expected bool, got NoneType\");\n}\n"
  },
  {
    "path": "crates/monty-cli/Cargo.toml",
    "content": "[package]\nname = \"monty-cli\"\nversion = { workspace = true }\nlicense = { workspace = true }\nrust-version = { workspace = true }\nedition = { workspace = true }\nauthors = { workspace = true }\ndescription = { workspace = true }\nkeywords = { workspace = true }\ncategories = { workspace = true }\nhomepage = { workspace = true }\nrepository = { workspace = true }\n\n[[bin]]\nname = \"monty\"\npath = \"src/main.rs\"\n\n[dependencies]\nclap = { version = \"4\", features = [\"derive\"] }\nmonty = { path = \"../monty\" }\nmonty_type_checking = { path = \"../monty-type-checking\" }\nrustyline = \"15\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/monty-cli/src/main.rs",
    "content": "use std::{\n    fmt, fs,\n    process::ExitCode,\n    time::{Duration, Instant},\n};\n\nuse clap::Parser;\nuse monty::{\n    LimitedTracker, MontyObject, MontyRepl, MontyRun, NameLookupResult, NoLimitTracker, PrintWriter,\n    ReplContinuationMode, ResourceLimits, ResourceTracker, RunProgress, detect_repl_continuation_mode,\n};\nuse rustyline::{DefaultEditor, error::ReadlineError};\n// disabled due to format failing on https://github.com/pydantic/monty/pull/75 where CI and local wanted imports ordered differently\n// TODO re-enabled soon!\n#[rustfmt::skip]\nuse monty_type_checking::{SourceFile, type_check};\n\n/// ANSI escape code for dim/gray text.\nconst DIM: &str = \"\\x1b[2m\";\n/// ANSI escape code for bold red text (errors).\nconst BOLD_RED: &str = \"\\x1b[1m\\x1b[31m\";\n/// ANSI escape code for bold green text (success, headings).\nconst BOLD_GREEN: &str = \"\\x1b[1m\\x1b[32m\";\n/// ANSI escape code for bold cyan text (commands, prompts).\nconst BOLD_CYAN: &str = \"\\x1b[1m\\x1b[36m\";\n/// ANSI escape code to reset all text styling.\nconst RESET: &str = \"\\x1b[0m\";\nconst ARROW: &str = \"❯\";\n\n/// Monty — a sandboxed Python interpreter written in Rust.\n///\n/// - `monty` starts an empty interactive REPL\n/// - `monty <file>` runs the file in script mode\n/// - `monty -c <cmd>` executes `<cmd>` as a Python program\n/// - `monty -i` starts an empty interactive REPL\n/// - `monty -i <file>` seeds the REPL with file contents\n#[derive(Parser)]\n#[command(version)]\nstruct Cli {\n    /// Start interactive REPL mode.\n    #[arg(short = 'i', long = \"interactive\")]\n    interactive: bool,\n\n    /// Run the type checker before executing.\n    #[arg(short = 't', long = \"type-check\")]\n    type_check: bool,\n\n    /// Execute a Python program passed as a string (like `python -c`).\n    #[arg(short = 'c')]\n    command: Option<String>,\n\n    /// Python file to execute.\n    file: Option<String>,\n\n    /// Maximum number of heap allocations before execution is terminated.\n    #[arg(long)]\n    max_allocations: Option<usize>,\n\n    /// Maximum execution time in seconds (e.g. `0.5` for 500ms).\n    #[arg(long)]\n    max_duration: Option<f64>,\n\n    /// Maximum heap memory (e.g. `1024`, `512KB`, `10MB`, `1GB`).\n    #[arg(long, value_parser = parse_memory_size)]\n    max_memory: Option<usize>,\n\n    /// Run garbage collection every N allocations.\n    #[arg(long)]\n    gc_interval: Option<usize>,\n\n    /// Maximum call-stack depth (defaults to 1000 when any limit is set).\n    #[arg(long)]\n    max_recursion_depth: Option<usize>,\n}\n\nimpl Cli {\n    /// Builds `ResourceLimits` from the parsed CLI arguments.\n    ///\n    /// Returns `None` when no resource flags were provided, which lets the\n    /// caller fall back to `NoLimitTracker` for zero-overhead execution.\n    fn resource_limits(&self) -> Option<ResourceLimits> {\n        if self.max_allocations.is_none()\n            && self.max_duration.is_none()\n            && self.max_memory.is_none()\n            && self.gc_interval.is_none()\n            && self.max_recursion_depth.is_none()\n        {\n            return None;\n        }\n\n        let mut limits = ResourceLimits::new();\n        if let Some(n) = self.max_allocations {\n            limits = limits.max_allocations(n);\n        }\n        if let Some(secs) = self.max_duration {\n            limits = limits.max_duration(Duration::from_secs_f64(secs));\n        }\n        if let Some(bytes) = self.max_memory {\n            limits = limits.max_memory(bytes);\n        }\n        if let Some(interval) = self.gc_interval {\n            limits = limits.gc_interval(interval);\n        }\n        if let Some(depth) = self.max_recursion_depth {\n            limits = limits.max_recursion_depth(Some(depth));\n        }\n        Some(limits)\n    }\n}\n\nconst EXT_FUNCTIONS: bool = false;\n\nfn main() -> ExitCode {\n    let cli = Cli::parse();\n\n    let type_check_enabled = cli.type_check;\n    let limits = cli.resource_limits();\n\n    if let Some(cmd) = cli.command {\n        if cli.file.is_some() {\n            eprintln!(\"{BOLD_RED}error{RESET}: cannot specify both -c and a file\");\n            return ExitCode::FAILURE;\n        }\n        return if cli.interactive {\n            dispatch_repl(\"<string>\", &cmd, limits)\n        } else {\n            dispatch_script(\"<string>\", cmd, type_check_enabled, limits)\n        };\n    }\n\n    if let Some(file_path) = cli.file.as_deref() {\n        let code = match read_file(file_path) {\n            Ok(code) => code,\n            Err(err) => {\n                eprintln!(\"{BOLD_RED}error{RESET}: {err}\");\n                return ExitCode::FAILURE;\n            }\n        };\n        return if cli.interactive {\n            dispatch_repl(file_path, &code, limits)\n        } else {\n            dispatch_script(file_path, code, type_check_enabled, limits)\n        };\n    }\n\n    dispatch_repl(\"repl.py\", \"\", limits)\n}\n\n/// Dispatches script execution with either `LimitedTracker` or `NoLimitTracker`.\n///\n/// This top-level branch avoids threading generics through the entire call chain\n/// while still keeping the zero-overhead `NoLimitTracker` path when no limits are set.\nfn dispatch_script(\n    file_path: &str,\n    code: String,\n    type_check_enabled: bool,\n    limits: Option<ResourceLimits>,\n) -> ExitCode {\n    if let Some(limits) = limits {\n        run_script(file_path, code, type_check_enabled, LimitedTracker::new(limits))\n    } else {\n        run_script(file_path, code, type_check_enabled, NoLimitTracker)\n    }\n}\n\n/// Dispatches REPL startup with either `LimitedTracker` or `NoLimitTracker`.\nfn dispatch_repl(file_path: &str, code: &str, limits: Option<ResourceLimits>) -> ExitCode {\n    if let Some(limits) = limits {\n        run_repl(file_path, code, LimitedTracker::new(limits))\n    } else {\n        run_repl(file_path, code, NoLimitTracker)\n    }\n}\n\n/// Executes a Python file in one-shot CLI mode.\n///\n/// This path keeps the existing CLI behavior: run type-checking for visibility,\n/// compile the file as a full module, and execute it either through direct\n/// execution or through the suspendable progress loop when external functions\n/// are enabled.\n///\n/// Returns `ExitCode::SUCCESS` for successful execution and\n/// `ExitCode::FAILURE` for parse/type/runtime failures.\nfn run_script(file_path: &str, code: String, type_check_enabled: bool, tracker: impl ResourceTracker) -> ExitCode {\n    if type_check_enabled {\n        let start = Instant::now();\n        if let Some(failure) = type_check(&SourceFile::new(&code, file_path), None).unwrap() {\n            let elapsed = start.elapsed();\n            eprintln!(\n                \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {BOLD_RED}type check failed{RESET}:\\n{failure}\",\n                FormattedDuration(elapsed)\n            );\n        } else {\n            let elapsed = start.elapsed();\n            eprintln!(\n                \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {BOLD_GREEN}type check passed{RESET}\",\n                FormattedDuration(elapsed)\n            );\n        }\n    }\n\n    let input_names = vec![];\n    let inputs = vec![];\n\n    let runner = match MontyRun::new(code, file_path, input_names) {\n        Ok(ex) => ex,\n        Err(err) => {\n            eprintln!(\"{BOLD_RED}error{RESET}:\\n{err}\");\n            return ExitCode::FAILURE;\n        }\n    };\n\n    if EXT_FUNCTIONS {\n        let start = Instant::now();\n        let progress = match runner.start(inputs, tracker, PrintWriter::Stdout) {\n            Ok(p) => p,\n            Err(err) => {\n                let elapsed = start.elapsed();\n                eprintln!(\n                    \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {BOLD_RED}error{RESET}: {err}\",\n                    FormattedDuration(elapsed)\n                );\n                return ExitCode::FAILURE;\n            }\n        };\n\n        match run_until_complete(progress) {\n            Ok(value) => {\n                let elapsed = start.elapsed();\n                eprintln!(\n                    \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {value}\",\n                    FormattedDuration(elapsed)\n                );\n                ExitCode::SUCCESS\n            }\n            Err(err) => {\n                let elapsed = start.elapsed();\n                eprintln!(\n                    \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {BOLD_RED}error{RESET}: {err}\",\n                    FormattedDuration(elapsed)\n                );\n                ExitCode::FAILURE\n            }\n        }\n    } else {\n        let start = Instant::now();\n        let value = match runner.run(inputs, tracker, PrintWriter::Stdout) {\n            Ok(p) => p,\n            Err(err) => {\n                let elapsed = start.elapsed();\n                eprintln!(\n                    \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {BOLD_RED}error{RESET}: {err}\",\n                    FormattedDuration(elapsed)\n                );\n                return ExitCode::FAILURE;\n            }\n        };\n        let elapsed = start.elapsed();\n        eprintln!(\n            \"{DIM}{}{RESET} {BOLD_CYAN}{ARROW}{RESET} {value}\",\n            FormattedDuration(elapsed)\n        );\n        ExitCode::SUCCESS\n    }\n}\n\n/// Starts an interactive line-by-line REPL session.\n///\n/// Initializes `MontyRepl` once and incrementally feeds entered snippets without\n/// replaying previous snippets, which matches the intended stateful REPL model.\n/// Multiline input follows CPython-style prompts:\n/// - `❯ ` for a new statement\n/// - `… ` for continuation lines\n///\n/// Returns `ExitCode::SUCCESS` on EOF or `exit`, and `ExitCode::FAILURE` on\n/// initialization or I/O errors.\nfn run_repl(file_path: &str, code: &str, tracker: impl ResourceTracker) -> ExitCode {\n    let mut repl = MontyRepl::new(file_path, tracker);\n\n    if !code.is_empty() {\n        execute_repl_snippet(&mut repl, code);\n    }\n\n    eprintln!(\"Monty v{} REPL. Type `exit` to exit.\", env!(\"CARGO_PKG_VERSION\"));\n\n    let mut rl = match DefaultEditor::new() {\n        Ok(rl) => rl,\n        Err(err) => {\n            eprintln!(\"{BOLD_RED}error{RESET} initializing editor: {err}\");\n            return ExitCode::FAILURE;\n        }\n    };\n\n    let mut pending_snippet = String::new();\n    let mut continuation_mode = ReplContinuationMode::Complete;\n\n    loop {\n        let prompt = if continuation_mode == ReplContinuationMode::Complete {\n            format!(\"{BOLD_CYAN}{ARROW}{RESET} \")\n        } else {\n            \"… \".to_owned()\n        };\n\n        let line = match rl.readline(&prompt) {\n            Ok(line) => line,\n            Err(ReadlineError::Eof) => return ExitCode::SUCCESS,\n            Err(ReadlineError::Interrupted) => {\n                // Ctrl-C: discard pending input and start fresh\n                pending_snippet.clear();\n                continuation_mode = ReplContinuationMode::Complete;\n                continue;\n            }\n            Err(err) => {\n                eprintln!(\"{BOLD_RED}error{RESET} reading input: {err}\");\n                return ExitCode::FAILURE;\n            }\n        };\n\n        let snippet = line.trim_end();\n        if continuation_mode == ReplContinuationMode::Complete && snippet.is_empty() {\n            continue;\n        }\n        if continuation_mode == ReplContinuationMode::Complete && snippet == \"exit\" {\n            return ExitCode::SUCCESS;\n        }\n\n        pending_snippet.push_str(snippet);\n        pending_snippet.push('\\n');\n\n        if continuation_mode == ReplContinuationMode::IncompleteBlock && snippet.is_empty() {\n            let _ = rl.add_history_entry(pending_snippet.trim_end());\n            execute_repl_snippet(&mut repl, &pending_snippet);\n            pending_snippet.clear();\n            continuation_mode = ReplContinuationMode::Complete;\n            continue;\n        }\n\n        let detected_mode = detect_repl_continuation_mode(&pending_snippet);\n        match detected_mode {\n            ReplContinuationMode::Complete => {\n                if continuation_mode == ReplContinuationMode::IncompleteBlock {\n                    continue;\n                }\n                let _ = rl.add_history_entry(pending_snippet.trim_end());\n                execute_repl_snippet(&mut repl, &pending_snippet);\n                pending_snippet.clear();\n                continuation_mode = ReplContinuationMode::Complete;\n            }\n            ReplContinuationMode::IncompleteBlock => continuation_mode = ReplContinuationMode::IncompleteBlock,\n            ReplContinuationMode::IncompleteImplicit => {\n                if continuation_mode != ReplContinuationMode::IncompleteBlock {\n                    continuation_mode = ReplContinuationMode::IncompleteImplicit;\n                }\n            }\n        }\n    }\n}\n\n/// Executes one collected REPL snippet, printing the result or error.\nfn execute_repl_snippet(repl: &mut MontyRepl<impl ResourceTracker>, snippet: &str) {\n    match repl.feed_run(snippet, vec![], PrintWriter::Stdout) {\n        Ok(output) => {\n            if output != MontyObject::None {\n                println!(\"{output}\");\n            }\n        }\n        Err(err) => {\n            eprintln!(\"{BOLD_RED}error{RESET}: {err}\");\n        }\n    }\n}\n\n/// Drives suspendable execution until completion.\n///\n/// This repeatedly resumes `RunProgress` values by resolving supported\n/// external calls and returns the final value when execution reaches\n/// `RunProgress::Complete`.\n///\n/// Returns an error string for unsupported suspend points (OS calls or async\n/// futures) or invalid external-function dispatch.\nfn run_until_complete(mut progress: RunProgress<impl ResourceTracker>) -> Result<MontyObject, String> {\n    loop {\n        match progress {\n            RunProgress::Complete(value) => return Ok(value),\n            RunProgress::FunctionCall(call) => {\n                let return_value = resolve_external_call(&call.function_name, &call.args)?;\n                progress = call\n                    .resume(return_value, PrintWriter::Stdout)\n                    .map_err(|err| format!(\"{err}\"))?;\n            }\n            RunProgress::ResolveFutures(state) => {\n                return Err(format!(\n                    \"async futures not supported in CLI: {:?}\",\n                    state.pending_call_ids()\n                ));\n            }\n            RunProgress::NameLookup(lookup) => {\n                let result = if lookup.name == \"add_ints\" {\n                    NameLookupResult::Value(MontyObject::Function {\n                        name: \"add_ints\".to_string(),\n                        docstring: None,\n                    })\n                } else {\n                    NameLookupResult::Undefined\n                };\n                progress = lookup\n                    .resume(result, PrintWriter::Stdout)\n                    .map_err(|err| format!(\"{err}\"))?;\n            }\n            RunProgress::OsCall(call) => {\n                return Err(format!(\n                    \"OS calls not supported in CLI: {:?}({:?})\",\n                    call.function, call.args\n                ));\n            }\n        }\n    }\n}\n\n/// Resolves supported CLI external function calls.\n///\n/// The CLI currently supports only `add_ints(int, int)`, which makes it\n/// possible to exercise the suspend/resume path in a deterministic way.\n///\n/// Returns a runtime-like error string for unknown function names, wrong arity,\n/// or incorrect argument types.\nfn resolve_external_call(function_name: &str, args: &[MontyObject]) -> Result<MontyObject, String> {\n    if function_name != \"add_ints\" {\n        return Err(format!(\"unknown external function: {function_name}({args:?})\"));\n    }\n\n    if args.len() != 2 {\n        return Err(format!(\"add_ints requires exactly 2 arguments, got {}\", args.len()));\n    }\n\n    if let (MontyObject::Int(a), MontyObject::Int(b)) = (&args[0], &args[1]) {\n        Ok(MontyObject::Int(a + b))\n    } else {\n        Err(format!(\"add_ints requires integer arguments, got {args:?}\"))\n    }\n}\n\n/// Reads a Python source file from disk, returning its contents as a string.\n///\n/// Returns an error message if the path doesn't exist, isn't a file, or can't be read.\nfn read_file(file_path: &str) -> Result<String, String> {\n    match fs::metadata(file_path) {\n        Ok(metadata) => {\n            if !metadata.is_file() {\n                return Err(format!(\"{file_path} is not a file\"));\n            }\n        }\n        Err(err) => {\n            return Err(format!(\"reading {file_path}: {err}\"));\n        }\n    }\n    match fs::read_to_string(file_path) {\n        Ok(contents) => Ok(contents),\n        Err(err) => Err(format!(\"reading file: {err}\")),\n    }\n}\n\n/// Wrapper around `Duration` that formats with 5 significant digits and an auto-selected unit.\n///\n/// - `< 1ms` → microseconds, e.g. `123.45μs`\n/// - `1ms..1s` → milliseconds, e.g. `12.345ms`\n/// - `≥ 1s` → seconds, e.g. `1.2345s`\n///\n/// The goal is a compact, human-readable duration string that stays consistent in width\n/// regardless of whether execution took microseconds or seconds.\nstruct FormattedDuration(Duration);\n\nimpl fmt::Display for FormattedDuration {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let duration = self.0;\n        let total_secs = duration.as_secs_f64();\n\n        if total_secs < 1e-3 {\n            // Microseconds\n            let us = total_secs * 1e6;\n            let decimals = sig_digits_after_decimal(us);\n            write!(f, \"{us:.decimals$}μs\")\n        } else if total_secs < 1.0 {\n            // Milliseconds\n            let ms = total_secs * 1e3;\n            let decimals = sig_digits_after_decimal(ms);\n            write!(f, \"{ms:.decimals$}ms\")\n        } else {\n            // Seconds\n            let decimals = sig_digits_after_decimal(total_secs);\n            write!(f, \"{total_secs:.decimals$}s\")\n        }\n    }\n}\n\n/// Calculates how many decimal places to show for 5 significant digits.\n///\n/// Counts the number of digits before the decimal point, then returns `5 - that count`\n/// (clamped to 0). For example, `12.345` has 2 digits before the decimal → 3 after = 5 total.\nfn sig_digits_after_decimal(value: f64) -> usize {\n    let before = if value < 1.0 {\n        1\n    } else {\n        // value is always positive and < 1e6 in practice, so log10 fits in a u32\n        #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]\n        let digits = (value.log10().floor() as u32) + 1;\n        digits as usize\n    };\n    5usize.saturating_sub(before)\n}\n\n/// Parses a memory size string with optional unit suffix.\n///\n/// Accepts plain byte counts (`1024`) or values with a case-insensitive suffix:\n/// `KB` (kilobytes), `MB` (megabytes), `GB` (gigabytes). The numeric part must\n/// be a valid `usize`.\n///\n/// # Examples\n///\n/// - `\"512\"` → 512\n/// - `\"512KB\"` → 524_288\n/// - `\"10MB\"` → 10_485_760\n/// - `\"1GB\"` → 1_073_741_824\nfn parse_memory_size(s: &str) -> Result<usize, String> {\n    let s = s.trim();\n    let (num_str, multiplier) = if let Some(n) = s.strip_suffix(\"GB\").or_else(|| s.strip_suffix(\"gb\")) {\n        (n.trim(), 1024 * 1024 * 1024)\n    } else if let Some(n) = s.strip_suffix(\"MB\").or_else(|| s.strip_suffix(\"mb\")) {\n        (n.trim(), 1024 * 1024)\n    } else if let Some(n) = s.strip_suffix(\"KB\").or_else(|| s.strip_suffix(\"kb\")) {\n        (n.trim(), 1024)\n    } else {\n        (s, 1)\n    };\n\n    let value: usize = num_str.parse().map_err(|e| format!(\"invalid memory size '{s}': {e}\"))?;\n\n    value\n        .checked_mul(multiplier)\n        .ok_or_else(|| format!(\"memory size '{s}' overflows\"))\n}\n"
  },
  {
    "path": "crates/monty-js/.cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\n"
  },
  {
    "path": "crates/monty-js/.gitignore",
    "content": "\n# Created by https://www.toptal.com/developers/gitignore/api/node\n# Edit at https://www.toptal.com/developers/gitignore?templates=node\n\n### Node ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# End of https://www.toptal.com/developers/gitignore/api/node\n\n\n#Added by cargo\n\n/target\nCargo.lock\n\n*.node\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/sdks\n!.yarn/versions\n/npm\n\n# napi generated js files\nbrowser.js\nindex.js\nindex.d.ts\nmonty.wasi.cjs\nmonty.wasi-browser.js\nwasi-worker.mjs\nwasi-worker-browser.mjs\n\n# tsc output (source is wrapper.ts)\nwrapper.js\nwrapper.d.ts\nwrapper.d.ts.map\nwrapper.js.map\n.claude/settings.local.json\nplans\nprompts\nmonty.wasm32-wasi.wasm\n"
  },
  {
    "path": "crates/monty-js/.prettierignore",
    "content": "target\n.yarn\nindex.js\npackage-template.wasi-browser.js\npackage-template.wasi.cjs\nwasi-worker-browser.mjs\nwasi-worker.mjs\n.yarnrc.yml\n"
  },
  {
    "path": "crates/monty-js/Cargo.toml",
    "content": "[package]\nname = \"monty-js\"\ndescription = \"TypeScript/JavaScript bindings for the Monty sandboxed Python interpreter\"\nreadme = \"README.md\"\nversion = { workspace = true }\nrust-version = { workspace = true }\n# edition = { workspace = true }\nedition = \"2021\"\nauthors = { workspace = true }\nlicense = { workspace = true }\nkeywords = { workspace = true }\ncategories = { workspace = true }\nhomepage = { workspace = true }\nrepository = { workspace = true }\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nmonty = { path = \"../monty\" }\nmonty_type_checking = { path = \"../monty-type-checking\" }\nnapi = { version = \"3.0.0\", default-features = false, features = [\"napi6\", \"compat-mode\"] }\nnapi-derive = \"3.0.0\"\nnum-bigint = { workspace = true }\nserde = { workspace = true }\npostcard = { workspace = true }\n\n[build-dependencies]\nnapi-build = \"2\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/monty-js/README.md",
    "content": "# @pydantic/monty\n\nJavaScript/TypeScript bindings for the Monty sandboxed Python interpreter.\n\n## Installation\n\n```bash\nnpm install @pydantic/monty\n```\n\n## Basic Usage\n\n```ts\nimport { Monty } from '@pydantic/monty'\n\n// Create interpreter and run code\nconst m = new Monty('1 + 2')\nconst result = m.run() // returns 3\n```\n\n## Input Variables\n\n```ts\nconst m = new Monty('x + y', { inputs: ['x', 'y'] })\nconst result = m.run({ inputs: { x: 10, y: 20 } }) // returns 30\n```\n\n## External Functions\n\nFor synchronous external functions, pass them directly to `run()`:\n\n```ts\nconst m = new Monty('add(2, 3)')\n\nconst result = m.run({\n  externalFunctions: {\n    add: (a: number, b: number) => a + b,\n  },\n}) // returns 5\n```\n\nFor async external functions, use `runMontyAsync()`:\n\n```ts\nimport { Monty, runMontyAsync } from '@pydantic/monty'\n\nconst m = new Monty('fetch_data(url)', {\n  inputs: ['url'],\n})\n\nconst result = await runMontyAsync(m, {\n  inputs: { url: 'https://example.com' },\n  externalFunctions: {\n    fetch_data: async (url: string) => {\n      const response = await fetch(url)\n      return response.text()\n    },\n  },\n})\n```\n\n## Iterative Execution\n\nFor fine-grained control over external function calls, use `start()` and `resume()`:\n\n```ts\nconst m = new Monty('a() + b()')\n\nlet progress = m.start()\nwhile (progress instanceof MontySnapshot) {\n  console.log(`Calling: ${progress.functionName}`)\n  console.log(`Args: ${progress.args}`)\n  // Provide the return value and resume\n  progress = progress.resume({ returnValue: 10 })\n}\n// progress is now MontyComplete\nconsole.log(progress.output) // 20\n```\n\n## Error Handling\n\n```ts\nimport { Monty, MontySyntaxError, MontyRuntimeError, MontyTypingError } from '@pydantic/monty'\n\ntry {\n  const m = new Monty('1 / 0')\n  m.run()\n} catch (error) {\n  if (error instanceof MontySyntaxError) {\n    console.log('Syntax error:', error.message)\n  } else if (error instanceof MontyRuntimeError) {\n    console.log('Runtime error:', error.message)\n    console.log('Traceback:', error.traceback())\n  } else if (error instanceof MontyTypingError) {\n    console.log('Type error:', error.displayDiagnostics())\n  }\n}\n```\n\n## Type Checking\n\n```ts\nconst m = new Monty('\"hello\" + 1')\ntry {\n  m.typeCheck()\n} catch (error) {\n  if (error instanceof MontyTypingError) {\n    console.log(error.displayDiagnostics('concise'))\n  }\n}\n\n// Or enable during construction\nconst m2 = new Monty('1 + 1', { typeCheck: true })\n```\n\n## Resource Limits\n\n```ts\nconst m = new Monty('1 + 1')\nconst result = m.run({\n  limits: {\n    maxAllocations: 10000,\n    maxDurationSecs: 5,\n    maxMemory: 1024 * 1024, // 1MB\n    maxRecursionDepth: 100,\n  },\n})\n```\n\n## Serialization\n\n```ts\n// Save parsed code to avoid re-parsing\nconst m = new Monty('complex_code()')\nconst data = m.dump()\n\n// Later, restore without re-parsing\nconst m2 = Monty.load(data)\nconst result = m2.run()\n\n// Snapshots can also be serialized\nconst snapshot = m.start()\nif (snapshot instanceof MontySnapshot) {\n  const snapshotData = snapshot.dump()\n  // Later, restore and resume\n  const restored = MontySnapshot.load(snapshotData)\n  const result = restored.resume({ returnValue: 42 })\n}\n```\n\n## API Reference\n\n### `Monty` Class\n\n- `constructor(code: string, options?: MontyOptions)` - Parse Python code\n- `run(options?: RunOptions)` - Execute and return the result\n- `start(options?: StartOptions)` - Start iterative execution\n- `typeCheck(prefixCode?: string)` - Perform static type checking\n- `dump()` - Serialize to binary format\n- `Monty.load(data)` - Deserialize from binary format\n- `scriptName` - The script name (default: `'main.py'`)\n- `inputs` - Declared input variable names\n\n### `MontyOptions`\n\n- `scriptName?: string` - Name used in tracebacks (default: `'main.py'`)\n- `inputs?: string[]` - Input variable names\n- `typeCheck?: boolean` - Enable type checking on construction\n- `typeCheckPrefixCode?: string` - Code to prepend for type checking\n\n### `RunOptions`\n\n- `inputs?: object` - Input variable values\n- `limits?: ResourceLimits` - Resource limits\n- `externalFunctions?: object` - External function callbacks\n\n### `ResourceLimits`\n\n- `maxAllocations?: number` - Maximum heap allocations\n- `maxDurationSecs?: number` - Maximum execution time in seconds\n- `maxMemory?: number` - Maximum heap memory in bytes\n- `gcInterval?: number` - Run GC every N allocations\n- `maxRecursionDepth?: number` - Maximum call stack depth (default: 1000)\n\n### `MontySnapshot` Class\n\nReturned by `start()` when execution pauses at an external function call.\n\n- `scriptName` - The script being executed\n- `functionName` - The external function being called\n- `args` - Positional arguments\n- `kwargs` - Keyword arguments\n- `resume(options: ResumeOptions)` - Resume with return value or exception\n- `dump()` / `MontySnapshot.load(data)` - Serialization\n\n### `MontyComplete` Class\n\nReturned by `start()` or `resume()` when execution completes.\n\n- `output` - The final result value\n\n### Error Classes\n\n- `MontyError` - Base class for all Monty errors\n- `MontySyntaxError` - Syntax/parsing errors\n- `MontyRuntimeError` - Runtime exceptions (with `traceback()`)\n- `MontyTypingError` - Type checking errors (with `displayDiagnostics()`)\n"
  },
  {
    "path": "crates/monty-js/__test__/async.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty, MontyRuntimeError, runMontyAsync } from '../wrapper'\n\n// =============================================================================\n// Basic async external function tests\n// =============================================================================\n\ntest('runMontyAsync with sync external function', async (t) => {\n  const m = new Monty('get_value()')\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      get_value: () => 42,\n    },\n  })\n\n  t.is(result, 42)\n})\n\ntest('runMontyAsync with async external function', async (t) => {\n  const m = new Monty('fetch_data()')\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      fetch_data: async () => {\n        // Simulate async operation\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        return 'async result'\n      },\n    },\n  })\n\n  t.is(result, 'async result')\n})\n\ntest('runMontyAsync with multiple async calls', async (t) => {\n  const m = new Monty(\n    `\na = fetch_a()\nb = fetch_b()\na + b\n`,\n    {},\n  )\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      fetch_a: async () => {\n        await new Promise((resolve) => setTimeout(resolve, 5))\n        return 10\n      },\n      fetch_b: async () => {\n        await new Promise((resolve) => setTimeout(resolve, 5))\n        return 20\n      },\n    },\n  })\n\n  t.is(result, 30)\n})\n\ntest('runMontyAsync with inputs', async (t) => {\n  const m = new Monty('multiply(x)', { inputs: ['x'] })\n\n  const result = await runMontyAsync(m, {\n    inputs: { x: 5 },\n    externalFunctions: {\n      multiply: async (n: number) => n * 2,\n    },\n  })\n\n  t.is(result, 10)\n})\n\ntest('runMontyAsync with args and kwargs', async (t) => {\n  const m = new Monty('process(1, 2, name=\"test\")')\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      process: async (a: number, b: number, kwargs: { name: string }) => {\n        return `${kwargs.name}: ${a + b}`\n      },\n    },\n  })\n\n  t.is(result, 'test: 3')\n})\n\n// =============================================================================\n// Error handling tests\n// =============================================================================\n\ntest('runMontyAsync sync function throws exception', async (t) => {\n  const m = new Monty('fail_sync()')\n\n  class ValueError extends Error {\n    override name = 'ValueError'\n  }\n\n  const error = await t.throwsAsync(\n    runMontyAsync(m, {\n      externalFunctions: {\n        fail_sync: () => {\n          throw new ValueError('sync error')\n        },\n      },\n    }),\n  )\n\n  t.true(error instanceof MontyRuntimeError)\n})\n\ntest('runMontyAsync async function throws exception', async (t) => {\n  const m = new Monty('fail_async()')\n\n  class ValueError extends Error {\n    override name = 'ValueError'\n  }\n\n  const error = await t.throwsAsync(\n    runMontyAsync(m, {\n      externalFunctions: {\n        fail_async: async () => {\n          await new Promise((resolve) => setTimeout(resolve, 5))\n          throw new ValueError('async error')\n        },\n      },\n    }),\n  )\n\n  t.true(error instanceof MontyRuntimeError)\n})\n\ntest('runMontyAsync exception caught in try/except', async (t) => {\n  const m = new Monty(\n    `\ntry:\n    might_fail()\nexcept ValueError:\n    result = 'caught'\nresult\n`,\n    {},\n  )\n\n  class ValueError extends Error {\n    override name = 'ValueError'\n  }\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      might_fail: async () => {\n        throw new ValueError('expected error')\n      },\n    },\n  })\n\n  t.is(result, 'caught')\n})\n\ntest('runMontyAsync missing external function raises NameError', async (t) => {\n  const m = new Monty('missing_func()')\n\n  const error = await t.throwsAsync(runMontyAsync(m, { externalFunctions: {} }))\n\n  t.true(error instanceof MontyRuntimeError)\n  t.true(error!.message.includes('NameError'))\n})\n\ntest('runMontyAsync missing function caught in try/except', async (t) => {\n  const m = new Monty(\n    `\ntry:\n    missing()\nexcept NameError:\n    result = 'caught'\nresult\n`,\n  )\n\n  const result = await runMontyAsync(m, { externalFunctions: {} })\n\n  t.is(result, 'caught')\n})\n\n// =============================================================================\n// Complex type tests\n// =============================================================================\n\ntest('runMontyAsync returns complex types', async (t) => {\n  const m = new Monty('get_data()')\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      get_data: async () => {\n        return [1, 2, { key: 'value' }]\n      },\n    },\n  })\n\n  t.true(Array.isArray(result))\n  t.is(result[0], 1)\n  t.is(result[1], 2)\n  t.true(result[2] instanceof Map)\n  t.is(result[2].get('key'), 'value')\n})\n\ntest('runMontyAsync with list input', async (t) => {\n  const m = new Monty('sum_list(items)', { inputs: ['items'] })\n\n  const result = await runMontyAsync(m, {\n    inputs: { items: [1, 2, 3, 4, 5] },\n    externalFunctions: {\n      sum_list: async (items: number[]) => {\n        return items.reduce((a, b) => a + b, 0)\n      },\n    },\n  })\n\n  t.is(result, 15)\n})\n\n// =============================================================================\n// Mixed sync/async tests\n// =============================================================================\n\ntest('runMontyAsync mixed sync and async functions', async (t) => {\n  const m = new Monty(\n    `\nsync_result = sync_func()\nasync_result = async_func()\nsync_result + async_result\n`,\n    {},\n  )\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      sync_func: () => 100,\n      async_func: async () => {\n        await new Promise((resolve) => setTimeout(resolve, 5))\n        return 200\n      },\n    },\n  })\n\n  t.is(result, 300)\n})\n\ntest('runMontyAsync chained async calls', async (t) => {\n  const m = new Monty(\n    `\nfirst = get_first()\nsecond = process(first)\nfinalize(second)\n`,\n    {},\n  )\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      get_first: async () => 'hello',\n      process: async (s: string) => s.toUpperCase(),\n      finalize: async (s: string) => `${s}!`,\n    },\n  })\n\n  t.is(result, 'HELLO!')\n})\n\n// =============================================================================\n// No external functions tests\n// =============================================================================\n\ntest('runMontyAsync without external functions', async (t) => {\n  const m = new Monty('1 + 2')\n\n  const result = await runMontyAsync(m, {})\n\n  t.is(result, 3)\n})\n\ntest('runMontyAsync pure computation', async (t) => {\n  const m = new Monty(\n    `\ndef factorial(n):\n    if n <= 1:\n        return 1\n    return n * factorial(n - 1)\nfactorial(5)\n`,\n  )\n\n  const result = await runMontyAsync(m)\n\n  t.is(result, 120)\n})\n\n// =============================================================================\n// printCallback tests\n// =============================================================================\n\ntest('runMontyAsync with printCallback', async (t) => {\n  const m = new Monty('print(\"hello from async\")')\n  const output: string[] = []\n\n  const result = await runMontyAsync(m, {\n    printCallback: (stream, text) => {\n      t.is(stream, 'stdout')\n      output.push(text)\n    },\n  })\n\n  t.is(result, null)\n  t.deepEqual(output, ['hello from async', '\\n'])\n})\n\ntest('runMontyAsync printCallback with external functions', async (t) => {\n  const m = new Monty('x = get_value()\\nprint(f\"got {x}\")\\nx', {\n    externalFunctions: ['get_value'],\n  })\n  const output: string[] = []\n\n  const result = await runMontyAsync(m, {\n    externalFunctions: {\n      get_value: () => 42,\n    },\n    printCallback: (stream, text) => {\n      t.is(stream, 'stdout')\n      output.push(text)\n    },\n  })\n\n  t.is(result, 42)\n  t.deepEqual(output, ['got 42', '\\n'])\n})\n\ntest('runMontyAsync printCallback with multiple prints', async (t) => {\n  const m = new Monty('print(\"a\")\\nprint(\"b\")\\nprint(\"c\")')\n  const output: string[] = []\n\n  await runMontyAsync(m, {\n    printCallback: (_stream, text) => {\n      output.push(text)\n    },\n  })\n\n  t.deepEqual(output, ['a', '\\n', 'b', '\\n', 'c', '\\n'])\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/basic.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty, MontySyntaxError } from '../wrapper'\n\n// =============================================================================\n// Constructor tests\n// =============================================================================\n\ntest('Monty constructor with default options', (t) => {\n  const m = new Monty('1 + 2')\n  t.is(m.scriptName, 'main.py')\n  t.deepEqual(m.inputs, [])\n})\n\ntest('Monty constructor with custom script name', (t) => {\n  const m = new Monty('1 + 2', { scriptName: 'test.py' })\n  t.is(m.scriptName, 'test.py')\n})\n\ntest('Monty constructor with inputs', (t) => {\n  const m = new Monty('x + y', { inputs: ['x', 'y'] })\n  t.deepEqual(m.inputs, ['x', 'y'])\n})\n\ntest('Monty constructor with syntax error', (t) => {\n  const error = t.throws(() => new Monty('def'), { instanceOf: MontySyntaxError })\n  t.true(error?.message.includes('SyntaxError'))\n})\n\n// =============================================================================\n// repr() tests\n// =============================================================================\n\ntest('Monty repr() no inputs', (t) => {\n  const m = new Monty('1 + 1')\n  const repr = m.repr()\n  t.true(repr.includes('Monty'))\n  t.true(repr.includes('main.py'))\n})\n\ntest('Monty repr() with inputs', (t) => {\n  const m = new Monty('x', { inputs: ['x', 'y'] })\n  const repr = m.repr()\n  t.true(repr.includes('Monty'))\n  t.true(repr.includes('inputs'))\n})\n\ntest('Monty repr() with inputs and external call', (t) => {\n  const m = new Monty('foo(x)', { inputs: ['x'] })\n  const repr = m.repr()\n  t.true(repr.includes('inputs'))\n})\n\n// =============================================================================\n// Simple expression tests\n// =============================================================================\n\ntest('simple expression', (t) => {\n  const m = new Monty('1 + 2')\n  t.is(m.run(), 3)\n})\n\ntest('arithmetic', (t) => {\n  const m = new Monty('10 * 5 - 3')\n  t.is(m.run(), 47)\n})\n\ntest('string concatenation', (t) => {\n  const m = new Monty('\"hello\" + \" \" + \"world\"')\n  t.is(m.run(), 'hello world')\n})\n\n// =============================================================================\n// Multiple runs tests\n// =============================================================================\n\ntest('multiple runs same instance', (t) => {\n  const m = new Monty('x * 2', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: 5 } }), 10)\n  t.is(m.run({ inputs: { x: 10 } }), 20)\n  t.is(m.run({ inputs: { x: -3 } }), -6)\n})\n\ntest('run multiple times no inputs', (t) => {\n  const m = new Monty('1 + 2')\n  t.is(m.run(), 3)\n  t.is(m.run(), 3)\n  t.is(m.run(), 3)\n})\n\n// =============================================================================\n// Multiline code tests\n// =============================================================================\n\ntest('multiline code', (t) => {\n  const code = `\nx = 1\ny = 2\nx + y\n`\n  const m = new Monty(code)\n  t.is(m.run(), 3)\n})\n\ntest('function definition and call', (t) => {\n  const code = `\ndef add(a, b):\n    return a + b\n\nadd(3, 4)\n`\n  const m = new Monty(code)\n  t.is(m.run(), 7)\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/exceptions.spec.ts",
    "content": "import test from 'ava'\n\nimport type { ErrorConstructor } from 'ava'\n\nimport { Monty, MontyError, MontySyntaxError, MontyRuntimeError, MontyTypingError } from '../wrapper'\n\n// Helper for asserting MontyRuntimeError, private constructor requires the awkward cast via any\n// but it works fine at runtime\nexport const isRuntimeError = { instanceOf: MontyRuntimeError as any as ErrorConstructor<MontyRuntimeError> }\n\n// =============================================================================\n// MontyRuntimeError tests\n// =============================================================================\n\ntest('zero division error', (t) => {\n  const m = new Monty('1 / 0')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'ZeroDivisionError: division by zero')\n})\n\ntest('value error', (t) => {\n  const m = new Monty('raise ValueError(\"bad value\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'ValueError: bad value')\n})\n\ntest('type error', (t) => {\n  const m = new Monty(\"'string' + 1\")\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.true(error.message.includes('TypeError'))\n})\n\ntest('index error', (t) => {\n  const m = new Monty('[1, 2, 3][10]')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'IndexError: list index out of range')\n})\n\ntest('key error', (t) => {\n  const m = new Monty('{\"a\": 1}[\"b\"]')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'KeyError: b')\n})\n\ntest('attribute error', (t) => {\n  const m = new Monty('raise AttributeError(\"no such attr\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'AttributeError: no such attr')\n})\n\ntest('name error', (t) => {\n  const m = new Monty('undefined_variable')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, \"NameError: name 'undefined_variable' is not defined\")\n})\n\ntest('assertion error', (t) => {\n  const m = new Monty('assert False')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.true(error.message.includes('AssertionError'))\n})\n\ntest('assertion error with message', (t) => {\n  const m = new Monty('assert False, \"custom message\"')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'AssertionError: custom message')\n})\n\ntest('runtime error', (t) => {\n  const m = new Monty('raise RuntimeError(\"runtime error\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'RuntimeError: runtime error')\n})\n\ntest('not implemented error', (t) => {\n  const m = new Monty('raise NotImplementedError(\"not implemented\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'NotImplementedError: not implemented')\n})\n\n// =============================================================================\n// OS call errors (no OS callback support in JS bindings)\n// =============================================================================\n\ntest('os.environ via run() raises NotImplementedError', (t) => {\n  const m = new Monty('import os\\nx = os.environ')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.exception.typeName, 'NotImplementedError')\n  t.is(error.exception.message, \"OS function 'os.environ' not implemented with standard execution\")\n})\n\ntest('os.getenv via run() raises NotImplementedError', (t) => {\n  const m = new Monty(\"import os\\nx = os.getenv('HOME')\")\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.exception.typeName, 'NotImplementedError')\n  t.is(error.exception.message, \"OS function 'os.getenv' not implemented with standard execution\")\n})\n\n// =============================================================================\n// MontySyntaxError tests\n// =============================================================================\n\ntest('syntax error on init', (t) => {\n  const error = t.throws(() => new Monty('def'), { instanceOf: MontySyntaxError })\n  t.true(error.message.includes('SyntaxError'))\n})\n\ntest('syntax error unclosed paren', (t) => {\n  const error = t.throws(() => new Monty('print(1'), { instanceOf: MontySyntaxError })\n  t.true(error.message.includes('SyntaxError'))\n})\n\ntest('syntax error invalid syntax', (t) => {\n  const error = t.throws(() => new Monty('x = = 1'), { instanceOf: MontySyntaxError })\n  t.true(error.message.includes('SyntaxError'))\n})\n\n// =============================================================================\n// Catching with base class tests\n// =============================================================================\n\ntest('catch with base class', (t) => {\n  const m = new Monty('1 / 0')\n  try {\n    m.run()\n    t.fail('Should have thrown')\n  } catch (e) {\n    t.true(e instanceof MontyError)\n  }\n})\n\ntest('catch syntax error with base class', (t) => {\n  try {\n    new Monty('def')\n  } catch (e) {\n    t.true(e instanceof MontyError)\n  }\n})\n\n// =============================================================================\n// Exception handling within Monty tests\n// =============================================================================\n\ntest('raise caught exception', (t) => {\n  const code = `\ntry:\n    1 / 0\nexcept ZeroDivisionError as e:\n    result = 'caught'\nresult\n`\n  const m = new Monty(code)\n  t.is(m.run(), 'caught')\n})\n\ntest('exception in function', (t) => {\n  const code = `\ndef fail():\n    raise ValueError('from function')\n\nfail()\n`\n  const m = new Monty(code)\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'ValueError: from function')\n})\n\n// =============================================================================\n// Display and str methods tests\n// =============================================================================\n\ntest('display traceback', (t) => {\n  const m = new Monty('1 / 0')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  const display = error.display('traceback')\n  t.true(display.includes('Traceback (most recent call last):'))\n  t.true(display.includes('ZeroDivisionError'))\n})\n\ntest('display type msg', (t) => {\n  const m = new Monty('raise ValueError(\"test message\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.display('type-msg'), 'ValueError: test message')\n})\n\ntest('runtime display', (t) => {\n  const m = new Monty('raise ValueError(\"test message\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.display('msg'), 'test message')\n  t.is(error.display('type-msg'), 'ValueError: test message')\n  const traceback = error.display('traceback')\n  t.true(traceback.includes('Traceback (most recent call last):'))\n  t.true(\n    traceback.includes(\"raise ValueError('test message')\") || traceback.includes('raise ValueError(\"test message\")'),\n  )\n  t.true(traceback.includes('ValueError: test message'))\n})\n\ntest('str returns type msg', (t) => {\n  const m = new Monty('raise ValueError(\"test message\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, 'ValueError: test message')\n})\n\ntest('syntax error display', (t) => {\n  const error = t.throws(() => new Monty('def'), { instanceOf: MontySyntaxError })\n  t.true(error.display().includes('Expected an identifier'))\n  t.true(error.display('type-msg').includes('SyntaxError'))\n})\n\n// =============================================================================\n// Traceback tests\n// =============================================================================\n\ntest('traceback frames', (t) => {\n  const code = `def inner():\n    raise ValueError('error')\n\ndef outer():\n    inner()\n\nouter()\n`\n  const m = new Monty(code)\n  const error = t.throws(() => m.run(), isRuntimeError)\n  const display = error.display('traceback')\n\n  t.true(display.includes('Traceback (most recent call last):'))\n  t.true(display.includes('outer()'))\n  t.true(display.includes('inner()'))\n  t.true(display.includes('ValueError: error'))\n})\n\n// =============================================================================\n// MontyError base class tests\n// =============================================================================\n\ntest('MontyError extends Error', (t) => {\n  const err = new MontyError('ValueError', 'test message')\n  t.true(err instanceof Error)\n  t.true(err instanceof MontyError)\n  t.is(err.name, 'MontyError')\n})\n\ntest('MontyError constructor and properties', (t) => {\n  const err = new MontyError('ValueError', 'test message')\n  t.deepEqual(err.exception, { typeName: 'ValueError', message: 'test message' })\n  t.is(err.message, 'ValueError: test message')\n})\n\ntest('MontyError display()', (t) => {\n  const err = new MontyError('ValueError', 'test message')\n  t.is(err.display('msg'), 'test message')\n  t.is(err.display('type-msg'), 'ValueError: test message')\n})\n\ntest('MontyError with empty message', (t) => {\n  const err = new MontyError('TypeError', '')\n  t.is(err.display('type-msg'), 'TypeError')\n})\n\n// =============================================================================\n// MontySyntaxError class tests\n// =============================================================================\n\ntest('MontySyntaxError extends MontyError and Error', (t) => {\n  const err = new MontySyntaxError('invalid syntax')\n  t.true(err instanceof Error)\n  t.true(err instanceof MontyError)\n  t.true(err instanceof MontySyntaxError)\n  t.is(err.name, 'MontySyntaxError')\n})\n\ntest('MontySyntaxError constructor and properties', (t) => {\n  const err = new MontySyntaxError('invalid syntax')\n  t.deepEqual(err.exception, { typeName: 'SyntaxError', message: 'invalid syntax' })\n  t.is(err.message, 'SyntaxError: invalid syntax')\n})\n\ntest('MontySyntaxError display()', (t) => {\n  const err = new MontySyntaxError('unexpected token')\n  t.is(err.display(), 'unexpected token')\n  t.is(err.display('msg'), 'unexpected token')\n  t.is(err.display('type-msg'), 'SyntaxError: unexpected token')\n})\n\n// =============================================================================\n// MontyRuntimeError class tests\n// =============================================================================\n\ntest('MontyRuntimeError display()', (t) => {\n  const m = new Monty('1 / 0')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.true(error instanceof MontyError)\n  t.true(error instanceof Error)\n\n  t.is(error.message, 'ZeroDivisionError: division by zero')\n\n  const traceback = error.display('traceback')\n  t.is(error.display(), traceback)\n  t.true(traceback.includes('Traceback (most recent call last):'))\n\n  t.is(error.display('type-msg'), 'ZeroDivisionError: division by zero')\n  t.is(error.display('msg'), 'division by zero')\n})\n\ntest('MontyRuntimeError can be caught with instanceof', (t) => {\n  const m = new Monty('1 / 0')\n  try {\n    m.run()\n    t.fail('Should have thrown')\n  } catch (e) {\n    t.true(e instanceof MontyRuntimeError)\n    t.true(e instanceof MontyError)\n    t.true(e instanceof Error)\n  }\n})\n\n// =============================================================================\n// MontyTypingError class tests\n// =============================================================================\n\ntest('MontyTypingError extends MontyError and Error', (t) => {\n  const err = new MontyTypingError('type mismatch')\n  t.true(err instanceof Error)\n  t.true(err instanceof MontyError)\n  t.true(err instanceof MontyTypingError)\n  t.is(err.name, 'MontyTypingError')\n})\n\ntest('MontyTypingError is thrown on type check failure', (t) => {\n  const code = `\nx: int = \"not an int\"\n`\n  const error = t.throws(() => new Monty(code, { typeCheck: true }), { instanceOf: MontyTypingError })\n  t.true(error instanceof MontyError)\n  t.true(error instanceof Error)\n})\n\n// =============================================================================\n// Error catching hierarchy tests\n// =============================================================================\n\ntest('MontyError catches all Monty exceptions', (t) => {\n  // Syntax error\n  try {\n    new Monty('def')\n  } catch (e) {\n    t.true(e instanceof MontyError)\n  }\n\n  // Runtime error\n  try {\n    new Monty('1 / 0').run()\n  } catch (e) {\n    t.true(e instanceof MontyError)\n  }\n\n  // Type error\n  try {\n    new Monty('x: int = \"str\"', { typeCheck: true })\n  } catch (e) {\n    t.true(e instanceof MontyError)\n  }\n})\n\ntest('can distinguish error types with instanceof', (t) => {\n  // Test syntax error\n  try {\n    new Monty('def')\n  } catch (e) {\n    t.true(e instanceof MontySyntaxError)\n    t.false(e instanceof MontyRuntimeError)\n    t.false(e instanceof MontyTypingError)\n  }\n\n  // Test runtime error\n  try {\n    new Monty('1 / 0').run()\n  } catch (e) {\n    t.true(e instanceof MontyRuntimeError)\n    t.false(e instanceof MontySyntaxError)\n    t.false(e instanceof MontyTypingError)\n  }\n\n  // Test type error\n  try {\n    new Monty('x: int = \"str\"', { typeCheck: true })\n  } catch (e) {\n    t.true(e instanceof MontyTypingError)\n    t.false(e instanceof MontySyntaxError)\n    t.false(e instanceof MontyRuntimeError)\n  }\n})\n\n// =============================================================================\n// Exception info accessors tests\n// =============================================================================\n\ntest('exception getter returns correct info for runtime error', (t) => {\n  const m = new Monty('raise ValueError(\"test\")')\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.exception.typeName, 'ValueError')\n  t.is(error.exception.message, 'test')\n})\n\ntest('exception getter returns correct info for syntax error', (t) => {\n  const error = t.throws(() => new Monty('def'), { instanceOf: MontySyntaxError })\n  t.is(error.exception.typeName, 'SyntaxError')\n})\n\n// =============================================================================\n// Polymorphic display() tests\n// =============================================================================\n\ntest('display() works polymorphically on MontyTypingError', (t) => {\n  try {\n    new Monty('x: int = \"str\"', { typeCheck: true })\n    t.fail('Should have thrown')\n  } catch (e) {\n    t.true(e instanceof MontyError)\n    const msg = (e as MontyError).display('msg')\n    t.true(msg.length > 0)\n    const typeMsg = (e as MontyError).display('type-msg')\n    t.true(typeMsg.startsWith('TypeError:'))\n  }\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/external.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty } from '../wrapper'\nimport { isRuntimeError } from './exceptions.spec'\n\n// =============================================================================\n// Basic external function tests\n// =============================================================================\n\ntest('external function no args', (t) => {\n  const m = new Monty('noop()')\n\n  const noop = (...args: unknown[]) => {\n    t.deepEqual(args, [])\n    return 'called'\n  }\n\n  const result = m.run({ externalFunctions: { noop } })\n  t.is(result, 'called')\n})\n\ntest('external function positional args', (t) => {\n  const m = new Monty('func(1, 2, 3)')\n\n  const func = (...args: unknown[]) => {\n    t.deepEqual(args, [1, 2, 3])\n    return 'ok'\n  }\n\n  t.is(m.run({ externalFunctions: { func } }), 'ok')\n})\n\ntest('external function kwargs only', (t) => {\n  const m = new Monty('func(a=1, b=\"two\")')\n\n  const func = (...args: unknown[]) => {\n    // kwargs are passed as the last argument as an object\n    t.deepEqual(args, [{ a: 1, b: 'two' }])\n    return 'ok'\n  }\n\n  t.is(m.run({ externalFunctions: { func } }), 'ok')\n})\n\ntest('external function mixed args kwargs', (t) => {\n  const m = new Monty('func(1, 2, x=\"hello\", y=True)')\n\n  const func = (...args: unknown[]) => {\n    // positional args followed by kwargs object\n    t.deepEqual(args, [1, 2, { x: 'hello', y: true }])\n    return 'ok'\n  }\n\n  t.is(m.run({ externalFunctions: { func } }), 'ok')\n})\n\ntest('external function complex types', (t) => {\n  const m = new Monty('func([1, 2], {\"key\": \"value\"})')\n\n  const func = (...args: unknown[]) => {\n    t.deepEqual(args[0], [1, 2])\n    // Dicts are returned as Maps\n    t.true(args[1] instanceof Map)\n    t.is((args[1] as Map<string, string>).get('key'), 'value')\n    return 'ok'\n  }\n\n  t.is(m.run({ externalFunctions: { func } }), 'ok')\n})\n\ntest('external function returns none', (t) => {\n  const m = new Monty('do_nothing()')\n\n  const do_nothing = () => {\n    // returns undefined which becomes None\n  }\n\n  t.is(m.run({ externalFunctions: { do_nothing } }), null)\n})\n\ntest('external function returns complex type', (t) => {\n  const m = new Monty('get_data()')\n\n  const get_data = () => {\n    return { a: [1, 2, 3], b: { nested: true } }\n  }\n\n  const result = m.run({ externalFunctions: { get_data } })\n  // Plain objects become Maps\n  t.true(result instanceof Map)\n  t.deepEqual(result.get('a'), [1, 2, 3])\n  const nested = result.get('b')\n  t.true(nested instanceof Map)\n  t.is(nested.get('nested'), true)\n})\n\n// =============================================================================\n// Multiple external functions tests\n// =============================================================================\n\ntest('multiple external functions', (t) => {\n  const m = new Monty('add(1, 2) + mul(3, 4)')\n\n  const add = (a: number, b: number) => {\n    t.is(a, 1)\n    t.is(b, 2)\n    return a + b\n  }\n\n  const mul = (a: number, b: number) => {\n    t.is(a, 3)\n    t.is(b, 4)\n    return a * b\n  }\n\n  const result = m.run({ externalFunctions: { add, mul } })\n  t.is(result, 15) // 3 + 12\n})\n\ntest('external function called multiple times', (t) => {\n  const m = new Monty('counter() + counter() + counter()')\n\n  let callCount = 0\n\n  const counter = () => {\n    callCount += 1\n    return callCount\n  }\n\n  const result = m.run({ externalFunctions: { counter } })\n  t.is(result, 6) // 1 + 2 + 3\n  t.is(callCount, 3)\n})\n\ntest('external function with input', (t) => {\n  const m = new Monty('process(x)', { inputs: ['x'] })\n\n  const process = (x: number) => {\n    t.is(x, 5)\n    return x * 10\n  }\n\n  t.is(m.run({ inputs: { x: 5 }, externalFunctions: { process } }), 50)\n})\n\n// =============================================================================\n// Error handling tests\n// =============================================================================\n\ntest('undeclared external function raises name error', (t) => {\n  const m = new Monty('missing()')\n\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, \"NameError: name 'missing' is not defined\")\n})\n\ntest('undeclared function raises name error', (t) => {\n  const m = new Monty('unknown_func()')\n\n  const error = t.throws(() => m.run(), isRuntimeError)\n  t.is(error.message, \"NameError: name 'unknown_func' is not defined\")\n})\n\ntest('external function raises exception', (t) => {\n  const m = new Monty('fail()')\n\n  const fail = () => {\n    const error = new Error('intentional error')\n    error.name = 'ValueError'\n    throw error\n  }\n\n  const error = t.throws(() => m.run({ externalFunctions: { fail } }), isRuntimeError)\n  t.true(error.message.includes('ValueError'))\n  t.true(error.message.includes('intentional error'))\n})\n\ntest('external function wrong name raises name error', (t) => {\n  // When 'foo' is called but only 'bar' is provided at runtime, foo is a NameError\n  // because no externalFunctions are declared in the constructor\n  const m = new Monty('foo()')\n\n  const bar = () => 1\n\n  const error = t.throws(() => m.run({ externalFunctions: { bar } }), isRuntimeError)\n  t.is(error.message, \"NameError: name 'foo' is not defined\")\n})\n\ntest('external function exception caught by try except', (t) => {\n  const code = `\ntry:\n    fail()\nexcept ValueError:\n    caught = True\ncaught\n`\n  const m = new Monty(code)\n\n  const fail = () => {\n    const error = new Error('caught error')\n    error.name = 'ValueError'\n    throw error\n  }\n\n  t.is(m.run({ externalFunctions: { fail } }), true)\n})\n\ntest('external function exception type preserved', (t) => {\n  const m = new Monty('fail()')\n\n  const fail = () => {\n    const error = new Error('type error message')\n    error.name = 'TypeError'\n    throw error\n  }\n\n  const error = t.throws(() => m.run({ externalFunctions: { fail } }), isRuntimeError)\n  t.true(error.message.includes('TypeError'))\n  t.true(error.message.includes('type error message'))\n})\n\n// =============================================================================\n// Exception hierarchy tests\n// =============================================================================\n\nconst exceptionTypes = [\n  'ZeroDivisionError',\n  'OverflowError',\n  'ArithmeticError',\n  'NotImplementedError',\n  'RecursionError',\n  'RuntimeError',\n  'KeyError',\n  'IndexError',\n  'LookupError',\n  'ValueError',\n  'TypeError',\n  'AttributeError',\n  'NameError',\n  'AssertionError',\n]\n\nfor (const exceptionType of exceptionTypes) {\n  test(`external function exception hierarchy - ${exceptionType}`, (t) => {\n    const m = new Monty('fail()')\n\n    const fail = () => {\n      const error = new Error('test message')\n      error.name = exceptionType\n      throw error\n    }\n\n    const error = t.throws(() => m.run({ externalFunctions: { fail } }), isRuntimeError)\n    t.true(error.message.includes(exceptionType))\n  })\n}\n\n// =============================================================================\n// Exception caught by parent tests\n// =============================================================================\n\nconst parentChildPairs: Array<[string, string]> = [\n  ['ZeroDivisionError', 'ArithmeticError'],\n  ['OverflowError', 'ArithmeticError'],\n  ['NotImplementedError', 'RuntimeError'],\n  ['RecursionError', 'RuntimeError'],\n  ['KeyError', 'LookupError'],\n  ['IndexError', 'LookupError'],\n]\n\nfor (const [childType, parentType] of parentChildPairs) {\n  test(`external function exception caught by parent - ${childType} caught by ${parentType}`, (t) => {\n    const code = `\ntry:\n    fail()\nexcept ${parentType}:\n    caught = 'parent'\nexcept ${childType}:\n    caught = 'child'\ncaught\n`\n    const m = new Monty(code)\n\n    const fail = () => {\n      const error = new Error('test')\n      error.name = childType\n      throw error\n    }\n\n    // Child exception should be caught by parent handler (which comes first)\n    t.is(m.run({ externalFunctions: { fail } }), 'parent')\n  })\n}\n\n// =============================================================================\n// Exception in various contexts\n// =============================================================================\n\ntest('external function exception in expression', (t) => {\n  const m = new Monty('1 + fail() + 2')\n\n  const fail = () => {\n    const error = new Error('mid-expression error')\n    error.name = 'RuntimeError'\n    throw error\n  }\n\n  const error = t.throws(() => m.run({ externalFunctions: { fail } }), isRuntimeError)\n  t.true(error.message.includes('RuntimeError'))\n  t.true(error.message.includes('mid-expression error'))\n})\n\ntest('external function exception after successful call', (t) => {\n  const code = `\na = success()\nb = fail()\na + b\n`\n  const m = new Monty(code)\n\n  const success = () => 10\n\n  const fail = () => {\n    const error = new Error('second call fails')\n    error.name = 'ValueError'\n    throw error\n  }\n\n  const error = t.throws(() => m.run({ externalFunctions: { success, fail } }), isRuntimeError)\n  t.true(error.message.includes('ValueError'))\n  t.true(error.message.includes('second call fails'))\n})\n\ntest('external function exception with finally', (t) => {\n  const code = `\nfinally_ran = False\ntry:\n    fail()\nexcept ValueError:\n    pass\nfinally:\n    finally_ran = True\nfinally_ran\n`\n  const m = new Monty(code)\n\n  const fail = () => {\n    const error = new Error('error')\n    error.name = 'ValueError'\n    throw error\n  }\n\n  t.is(m.run({ externalFunctions: { fail } }), true)\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/inputs.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty } from '../wrapper'\n\n// =============================================================================\n// Single input tests\n// =============================================================================\n\ntest('single input', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: 42 } }), 42)\n})\n\ntest('multiple inputs', (t) => {\n  const m = new Monty('x + y + z', { inputs: ['x', 'y', 'z'] })\n  t.is(m.run({ inputs: { x: 1, y: 2, z: 3 } }), 6)\n})\n\ntest('input used in expression', (t) => {\n  const m = new Monty('x * 2 + y', { inputs: ['x', 'y'] })\n  t.is(m.run({ inputs: { x: 5, y: 3 } }), 13)\n})\n\ntest('input string', (t) => {\n  const m = new Monty('greeting + \" \" + name', { inputs: ['greeting', 'name'] })\n  t.is(m.run({ inputs: { greeting: 'Hello', name: 'World' } }), 'Hello World')\n})\n\ntest('input list', (t) => {\n  const m = new Monty('data[0] + data[1]', { inputs: ['data'] })\n  t.is(m.run({ inputs: { data: [10, 20] } }), 30)\n})\n\ntest('input dict', (t) => {\n  const m = new Monty('config[\"a\"] * config[\"b\"]', { inputs: ['config'] })\n  t.is(m.run({ inputs: { config: { a: 3, b: 4 } } }), 12)\n})\n\n// =============================================================================\n// Missing input tests\n// =============================================================================\n\ntest('missing input raises', (t) => {\n  const m = new Monty('x + y', { inputs: ['x', 'y'] })\n  const error = t.throws(() => m.run({ inputs: { x: 1 } }))\n  t.true(error?.message.includes('Missing required input'))\n})\n\ntest('all inputs missing raises', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  const error = t.throws(() => m.run())\n  t.true(error?.message.includes('Missing required input'))\n})\n\ntest('no inputs declared but provided raises', (t) => {\n  const m = new Monty('1 + 1')\n  const error = t.throws(() => m.run({ inputs: { x: 1 } }))\n  t.true(error?.message.includes('No input variables declared'))\n})\n\n// =============================================================================\n// Input order tests\n// =============================================================================\n\ntest('inputs order independent', (t) => {\n  const m = new Monty('a - b', { inputs: ['a', 'b'] })\n  // Dict order shouldn't matter\n  t.is(m.run({ inputs: { b: 3, a: 10 } }), 7)\n})\n\n// =============================================================================\n// Function parameter shadowing tests\n// =============================================================================\n\ntest('function param shadows input', (t) => {\n  const code = `\ndef foo(x):\n    return x + 1\n\nfoo(x * 2)\n`\n  const m = new Monty(code, { inputs: ['x'] })\n  // x=5, so foo(x * 2) = foo(10), and inside foo, x is 10 (not 5), so returns 11\n  t.is(m.run({ inputs: { x: 5 } }), 11)\n})\n\ntest('function param shadows input multiple params', (t) => {\n  const code = `\ndef add(x, y):\n    return x + y\n\nadd(x * 10, y * 100)\n`\n  const m = new Monty(code, { inputs: ['x', 'y'] })\n  // x=2, y=3, so add(20, 300) should return 320\n  t.is(m.run({ inputs: { x: 2, y: 3 } }), 320)\n})\n\ntest('input accessible outside shadowing function', (t) => {\n  const code = `\ndef double(x):\n    return x * 2\n\nresult = double(10) + x\nresult\n`\n  const m = new Monty(code, { inputs: ['x'] })\n  // double(10) = 20, x (input) = 5, so result = 25\n  t.is(m.run({ inputs: { x: 5 } }), 25)\n})\n\ntest('function param shadows input with default', (t) => {\n  const code = `\ndef foo(x=100):\n    return x + 1\n\nfoo(x * 2)\n`\n  const m = new Monty(code, { inputs: ['x'] })\n  // x=5, foo(10), inside foo x=10 (not 5 or 100), returns 11\n  t.is(m.run({ inputs: { x: 5 } }), 11)\n})\n\ntest('function uses input directly', (t) => {\n  const code = `\ndef foo(y):\n    return x + y\n\nfoo(10)\n`\n  const m = new Monty(code, { inputs: ['x'] })\n  // x=5 (input), foo(10) with y=10, returns x + y = 5 + 10 = 15\n  t.is(m.run({ inputs: { x: 5 } }), 15)\n})\n\n// =============================================================================\n// Complex input types tests\n// =============================================================================\n\ntest('complex input types', (t) => {\n  const m = new Monty('len(items)', { inputs: ['items'] })\n  t.is(m.run({ inputs: { items: [1, 2, 3, 4, 5] } }), 5)\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/limits.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty, MontyRuntimeError, type ResourceLimits } from '../wrapper'\n\n// =============================================================================\n// ResourceLimits construction tests\n// =============================================================================\n\ntest('resource limits custom', (t) => {\n  const limits: ResourceLimits = {\n    maxAllocations: 100,\n    maxDurationSecs: 5.0,\n    maxMemory: 1024,\n    gcInterval: 10,\n    maxRecursionDepth: 500,\n  }\n  // Just verify the object is valid and can be passed\n  const m = new Monty('1 + 1')\n  t.is(m.run({ limits }), 2)\n})\n\ntest('run with limits', (t) => {\n  const m = new Monty('1 + 1')\n  const limits: ResourceLimits = { maxDurationSecs: 5.0 }\n  t.is(m.run({ limits }), 2)\n})\n\n// =============================================================================\n// Recursion limit tests\n// =============================================================================\n\ntest('recursion limit', (t) => {\n  const code = `\ndef recurse(n):\n    if n <= 0:\n        return 0\n    return 1 + recurse(n - 1)\n\nrecurse(10)\n`\n  const m = new Monty(code)\n  const limits: ResourceLimits = { maxRecursionDepth: 5 }\n  const error = t.throws(() => m.run({ limits }), { instanceOf: MontyRuntimeError })\n  t.true(error.message.includes('RecursionError'))\n})\n\ntest('recursion limit ok', (t) => {\n  const code = `\ndef recurse(n):\n    if n <= 0:\n        return 0\n    return 1 + recurse(n - 1)\n\nrecurse(5)\n`\n  const m = new Monty(code)\n  const limits: ResourceLimits = { maxRecursionDepth: 100 }\n  t.is(m.run({ limits }), 5)\n})\n\n// =============================================================================\n// Allocation limit tests\n// =============================================================================\n\ntest('allocation limit', (t) => {\n  // Use a more aggressive allocation pattern\n  const code = `\nresult = []\nfor i in range(10000):\n    result.append([i])\nlen(result)\n`\n  const m = new Monty(code)\n  const limits: ResourceLimits = { maxAllocations: 5 }\n  const error = t.throws(() => m.run({ limits }), { instanceOf: MontyRuntimeError })\n  t.true(error.message.includes('MemoryError'))\n})\n\n// =============================================================================\n// Memory limit tests\n// =============================================================================\n\ntest('memory limit', (t) => {\n  const code = `\nresult = []\nfor i in range(1000):\n    result.append('x' * 100)\nlen(result)\n`\n  const m = new Monty(code)\n  const limits: ResourceLimits = { maxMemory: 100 }\n  const error = t.throws(() => m.run({ limits }), { instanceOf: MontyRuntimeError })\n  t.true(error.message.includes('MemoryError'))\n})\n\n// =============================================================================\n// Limits with inputs tests\n// =============================================================================\n\ntest('limits with inputs', (t) => {\n  const m = new Monty('x * 2', { inputs: ['x'] })\n  const limits: ResourceLimits = { maxDurationSecs: 5.0 }\n  t.is(m.run({ inputs: { x: 21 }, limits }), 42)\n})\n\n// =============================================================================\n// Large operation limits tests\n// =============================================================================\n\ntest('pow memory limit', (t) => {\n  const m = new Monty('2 ** 10000000')\n  const limits: ResourceLimits = { maxMemory: 1_000_000 }\n  const error = t.throws(() => m.run({ limits }), { instanceOf: MontyRuntimeError })\n  t.true(error.message.includes('MemoryError'))\n})\n\ntest('lshift memory limit', (t) => {\n  const m = new Monty('1 << 10000000')\n  const limits: ResourceLimits = { maxMemory: 1_000_000 }\n  const error = t.throws(() => m.run({ limits }), { instanceOf: MontyRuntimeError })\n  t.true(error.message.includes('MemoryError'))\n})\n\ntest('mult memory limit', (t) => {\n  const code = `\nbig = 2 ** 4000000\nresult = big * big\n`\n  const m = new Monty(code)\n  const limits: ResourceLimits = { maxMemory: 1_000_000 }\n  const error = t.throws(() => m.run({ limits }), { instanceOf: MontyRuntimeError })\n  t.true(error.message.includes('MemoryError'))\n})\n\ntest('small operations within limit', (t) => {\n  const m = new Monty('2 ** 1000')\n  const limits: ResourceLimits = { maxMemory: 1_000_000 }\n  const result = m.run({ limits })\n  t.true(typeof result === 'bigint' || typeof result === 'number')\n})\n\n// =============================================================================\n// Time limit tests\n// =============================================================================\n\ntest('time limit', (t) => {\n  // Use recursion instead of while loop\n  const code = `\ndef infinite(n):\n    return infinite(n + 1)\ninfinite(0)\n`\n  const m = new Monty(code)\n  const limits: ResourceLimits = { maxDurationSecs: 0.1 }\n  const error = t.throws(() => m.run({ limits }))\n  // May hit time limit or recursion limit\n  t.true(\n    error?.message.includes('TimeoutError') ||\n      error?.message.includes('timed out') ||\n      error?.message.includes('RecursionError'),\n  )\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/package.json",
    "content": "{\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "crates/monty-js/__test__/print.spec.ts",
    "content": "import type { ExecutionContext } from 'ava'\nimport test from 'ava'\nimport { Monty, type ResourceLimits, MontySnapshot, MontyComplete } from '../wrapper'\n\n// =============================================================================\n// Print tests\n// =============================================================================\n\nfunction makePrintCollector(t: ExecutionContext) {\n  const output: string[] = []\n\n  const callback = (stream: string, text: string) => {\n    t.is(stream, 'stdout')\n    output.push(text)\n  }\n\n  return { callback, output }\n}\n\ntest('basic', (t) => {\n  const m = new Monty('print(\"hello\")')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), 'hello\\n')\n})\n\ntest('multiple', (t) => {\n  const m = new Monty('print(\"hello\")\\nprint(\"world\")')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), 'hello\\nworld\\n')\n})\n\ntest('with values', (t) => {\n  const m = new Monty('print(\"The answer is\", 42)')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), 'The answer is 42\\n')\n})\n\ntest('with step', (t) => {\n  const m = new Monty('print(1, 2, 3, sep=\"-\")')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), '1-2-3\\n')\n})\n\ntest('with end', (t) => {\n  const m = new Monty('print(\"hello\", end=\"!\")')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), 'hello!')\n})\n\ntest('returns none', (t) => {\n  const m = new Monty('result = print(\"hello\")')\n  const { callback } = makePrintCollector(t)\n  const result = m.run({ printCallback: callback })\n  t.is(result, null)\n})\n\ntest('empty', (t) => {\n  const m = new Monty('print()')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), '\\n')\n})\n\ntest('with limits', (t) => {\n  const m = new Monty('print(\"with limits\")')\n  const { output, callback } = makePrintCollector(t)\n  const limits: ResourceLimits = {\n    maxDurationSecs: 5.0,\n  }\n  m.run({ printCallback: callback, limits })\n  t.is(output.join(''), 'with limits\\n')\n})\n\ntest('with inputs', (t) => {\n  const m = new Monty('print(\"Input value is\", x)', { inputs: ['x'] })\n  const { output, callback } = makePrintCollector(t)\n  m.run({ inputs: { x: 99 }, printCallback: callback })\n  t.is(output.join(''), 'Input value is 99\\n')\n})\n\ntest('print in loop', (t) => {\n  const code = `\nfor i in range(3):\n\tprint(\"Count\", i)\n`\n  const m = new Monty(code)\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), 'Count 0\\nCount 1\\nCount 2\\n')\n})\n\ntest('print mixed types', (t) => {\n  const m = new Monty('print(\"Value:\", 3.14, True, None, [1, 2, 3])')\n  const { output, callback } = makePrintCollector(t)\n  m.run({ printCallback: callback })\n  t.is(output.join(''), 'Value: 3.14 True None [1, 2, 3]\\n')\n})\n\nfunction makeErrorCallback(error: Error, t: ExecutionContext) {\n  const output: string[] = []\n\n  const callback = (stream: string, text: string) => {\n    const _ignore = text\n    t.is(stream, 'stdout')\n    throw error\n  }\n\n  return { callback, output }\n}\n\ntest('raises error', (t) => {\n  const m = new Monty('print(\"This will error\")')\n  const error = new Error('Custom print error')\n  const { callback } = makeErrorCallback(error, t)\n  const thrown = t.throws(() => {\n    m.run({ printCallback: callback })\n  })\n  // the error is slightly different with WASI, it doesn't include \"Error: \"\n  t.regex(thrown?.message, /Exception: (:?Error: )?Custom print error/)\n})\n\ntest('raises in function', (t) => {\n  const code = `\ndef greet(name):\n\tprint(f\"Hello, {name}!\")\n\ngreet(\"Alice\")\n`\n  const m = new Monty(code)\n  const error = new Error('Print error in function')\n  const { callback } = makeErrorCallback(error, t)\n  const thrown = t.throws(() => {\n    m.run({ printCallback: callback })\n  })\n  // the error is slightly different with WASI, it doesn't include \"Error: \"\n  t.regex(thrown?.message, /Exception: (:?Error: )?Print error in function/)\n})\n\ntest('raises in nested function', (t) => {\n  const code = `\ndef outer():\n\tdef inner():\n\t\tprint(\"Inside inner function\")\n\tinner()\n\nouter()\n`\n  const m = new Monty(code)\n  const error = new Error('Print error in nested function')\n  const { callback } = makeErrorCallback(error, t)\n  const thrown = t.throws(() => {\n    m.run({ printCallback: callback })\n  })\n  // the error is slightly different with WASI, it doesn't include \"Error: \"\n  t.regex(thrown?.message, /Exception: (:?Error: )?Print error in nested function/)\n})\n\ntest('raises in loop', (t) => {\n  const code = `\nfor i in range(3):\n\tprint(f\"Count: {i}\")\n`\n  const m = new Monty(code)\n  const error = new Error('Print error in loop')\n  const { callback } = makeErrorCallback(error, t)\n  const thrown = t.throws(() => {\n    m.run({ printCallback: callback })\n  })\n  // the error is slightly different with WASI, it doesn't include \"Error: \"\n  t.regex(thrown?.message, /Exception: (:?Error: )?Print error in loop/)\n})\n\ntest('with snapshot', (t) => {\n  const m = new Monty('print(\"snapshot\")')\n  const { output, callback } = makePrintCollector(t)\n  const result = m.start({\n    printCallback: callback,\n  })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, null)\n  t.is(output.join(''), 'snapshot\\n')\n})\n\ntest('with snapshot resume', (t) => {\n  const code = `\nprint(\"hello\")\nprint(func())\n`\n  const m = new Monty(code)\n  const { output, callback } = makePrintCollector(t)\n  const progress = m.start({\n    printCallback: callback,\n  })\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  const result = snapshot.resume({\n    returnValue: 'world',\n  })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, null)\n  t.is(output.join(''), 'hello\\nworld\\n')\n})\n\ntest('with snapshot dump load', (t) => {\n  const m = new Monty('print(func())')\n  const { output, callback } = makePrintCollector(t)\n\n  const progress = m.start({\n    printCallback: callback,\n  })\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  const data = snapshot.dump()\n\n  const progress2 = MontySnapshot.load(data, {\n    printCallback: callback,\n  })\n  const result = progress2.resume({\n    returnValue: 42,\n  })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, null)\n  t.is(output.join(''), '42\\n')\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/repl.spec.ts",
    "content": "import test from 'ava'\n\nimport { MontyRepl } from '../wrapper'\n\ntest('feed preserves state without replay', (t) => {\n  const repl = new MontyRepl()\n\n  repl.feed('counter = 0')\n  t.is(repl.feed('counter = counter + 1'), null)\n  t.is(repl.feed('counter'), 1)\n  t.is(repl.feed('counter = counter + 1'), null)\n  t.is(repl.feed('counter'), 2)\n})\n\ntest('constructor accepts scriptName option', (t) => {\n  const repl = new MontyRepl({ scriptName: 'test.py' })\n  t.is(repl.scriptName, 'test.py')\n})\n\ntest('default scriptName is main.py', (t) => {\n  const repl = new MontyRepl()\n  t.is(repl.scriptName, 'main.py')\n})\n\ntest('repl dump/load roundtrip', (t) => {\n  const repl = new MontyRepl()\n  repl.feed('x = 40')\n  t.is(repl.feed('x = x + 1'), null)\n\n  const serialized = repl.dump()\n  const loaded = MontyRepl.load(serialized)\n\n  t.is(loaded.feed('x + 1'), 42)\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/serialize.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty, MontySnapshot, MontyNameLookup, MontyComplete, type ResourceLimits } from '../wrapper'\nimport { Buffer } from 'node:buffer'\n\n// =============================================================================\n// Monty dump/load tests\n// =============================================================================\n\ntest('monty dump load roundtrip', (t) => {\n  const m = new Monty('x + 1', { inputs: ['x'] })\n  const data = m.dump()\n\n  t.true(data instanceof Buffer)\n  t.true(data.length > 0)\n\n  const m2 = Monty.load(data)\n  t.is(m2.run({ inputs: { x: 41 } }), 42)\n})\n\ntest('monty dump load preserves script name', (t) => {\n  const m = new Monty('1', { scriptName: 'custom.py' })\n  const data = m.dump()\n\n  const m2 = Monty.load(data)\n  t.is(m2.scriptName, 'custom.py')\n})\n\ntest('monty dump load preserves inputs', (t) => {\n  const m = new Monty('x + y', { inputs: ['x', 'y'] })\n  const data = m.dump()\n\n  const m2 = Monty.load(data)\n  t.deepEqual(m2.inputs, ['x', 'y'])\n  t.is(m2.run({ inputs: { x: 1, y: 2 } }), 3)\n})\n\ntest('monty dump load preserves code execution', (t) => {\n  const m = new Monty('func()')\n  const data = m.dump()\n\n  const m2 = Monty.load(data)\n  const progress = m2.start()\n  t.true(progress instanceof MontySnapshot)\n  t.is((progress as MontySnapshot).functionName, 'func')\n})\n\ntest('monty dump produces same result on multiple calls', (t) => {\n  const m = new Monty('1 + 2')\n  const bytes1 = m.dump()\n  const bytes2 = m.dump()\n  t.deepEqual(bytes1, bytes2)\n})\n\ntest('monty dump load various outputs', (t) => {\n  const testCases: Array<[string, unknown]> = [\n    ['1 + 1', 2],\n    ['\"hello\"', 'hello'],\n    ['[1, 2, 3]', [1, 2, 3]],\n    ['True', true],\n    ['None', null],\n  ]\n\n  for (const [code, expected] of testCases) {\n    const m = new Monty(code)\n    const data = m.dump()\n    const m2 = Monty.load(data)\n    t.deepEqual(m2.run(), expected)\n  }\n})\n\n// =============================================================================\n// MontySnapshot dump/load tests\n// =============================================================================\n\ntest('snapshot dump load roundtrip', (t) => {\n  const m = new Monty('func(1, 2)')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n\n  t.is(snapshot.functionName, 'func')\n  t.deepEqual(snapshot.args, [1, 2])\n  t.deepEqual(snapshot.kwargs, {})\n\n  const data = snapshot.dump()\n  t.true(data instanceof Buffer)\n  t.true(data.length > 0)\n\n  const snapshot2 = MontySnapshot.load(data)\n  t.is(snapshot2.functionName, 'func')\n  t.deepEqual(snapshot2.args, [1, 2])\n  t.deepEqual(snapshot2.kwargs, {})\n\n  const result = snapshot2.resume({ returnValue: 100 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 100)\n})\n\ntest('snapshot dump load preserves script name', (t) => {\n  const m = new Monty('func()', { scriptName: 'test.py' })\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n\n  const data = (progress as MontySnapshot).dump()\n  const progress2 = MontySnapshot.load(data)\n  t.is(progress2.scriptName, 'test.py')\n})\n\ntest('snapshot dump load with kwargs', (t) => {\n  const m = new Monty('func(a=1, b=\"hello\")')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n\n  const data = (progress as MontySnapshot).dump()\n  const progress2 = MontySnapshot.load(data)\n  t.is(progress2.functionName, 'func')\n  t.deepEqual(progress2.args, [])\n  t.deepEqual(progress2.kwargs, { a: 1, b: 'hello' })\n})\n\ntest('snapshot dump after resume fails', (t) => {\n  const m = new Monty('func()')\n  const snapshot = m.start() as MontySnapshot\n\n  snapshot.resume({ returnValue: 1 })\n\n  const error = t.throws(() => snapshot.dump())\n  t.true(error?.message.includes('already been resumed'))\n})\n\ntest('snapshot dump load multiple calls', (t) => {\n  const m = new Monty('a() + b()')\n\n  // First call: a()\n  let progress: MontySnapshot | MontyNameLookup | MontyComplete = m.start()\n  t.true(progress instanceof MontySnapshot)\n  let snapshot = progress as MontySnapshot\n  t.is(snapshot.functionName, 'a')\n\n  // Dump and load the state\n  const data = snapshot.dump()\n  snapshot = MontySnapshot.load(data)\n\n  // Resume with first return value — triggers b()\n  progress = snapshot.resume({ returnValue: 10 })\n  t.true(progress instanceof MontySnapshot)\n  let snapshot2 = progress as MontySnapshot\n  t.is(snapshot2.functionName, 'b')\n\n  // Dump and load again\n  const data2 = snapshot2.dump()\n  snapshot2 = MontySnapshot.load(data2)\n\n  // Resume with second return value\n  const result = snapshot2.resume({ returnValue: 5 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 15)\n})\n\ntest('snapshot dump load with limits', (t) => {\n  const m = new Monty('func()')\n  const limits: ResourceLimits = { maxAllocations: 1000 }\n  const progress = m.start({ limits })\n  t.true(progress instanceof MontySnapshot)\n\n  const data = (progress as MontySnapshot).dump()\n  const progress2 = MontySnapshot.load(data)\n\n  const result = progress2.resume({ returnValue: 99 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 99)\n})\n\n// =============================================================================\n// MontyNameLookup dump/load tests\n// =============================================================================\n\ntest('name lookup dump load roundtrip', (t) => {\n  const m = new Monty('x = foo; x')\n  const lookup = m.start()\n  t.true(lookup instanceof MontyNameLookup)\n\n  const data = (lookup as MontyNameLookup).dump()\n  t.true(data instanceof Buffer)\n  t.true(data.length > 0)\n\n  const lookup2 = MontyNameLookup.load(data)\n  t.is(lookup2.variableName, 'foo')\n  t.is(lookup2.scriptName, 'main.py')\n\n  const result = lookup2.resume({ value: 42 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 42)\n})\n\ntest('name lookup dump after resume fails', (t) => {\n  const m = new Monty('x = foo; x')\n  const lookup = m.start() as MontyNameLookup\n\n  lookup.resume({ value: 42 })\n\n  const error = t.throws(() => lookup.dump())\n  t.true(error?.message.includes('already been resumed'))\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/start.spec.ts",
    "content": "import test from 'ava'\n\nimport {\n  Monty,\n  MontySnapshot,\n  MontyNameLookup,\n  MontyComplete,\n  MontyRuntimeError,\n  type ResourceLimits,\n  type ResumeOptions,\n} from '../wrapper'\n\n// =============================================================================\n// start() returns MontyComplete tests\n// =============================================================================\n\ntest('start no external functions returns complete', (t) => {\n  const m = new Monty('1 + 2')\n  const result = m.start()\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 3)\n})\n\ntest('start returns complete for various types', (t) => {\n  const testCases: Array<[string, unknown]> = [\n    ['1', 1],\n    ['\"hello\"', 'hello'],\n    ['[1, 2, 3]', [1, 2, 3]],\n    ['None', null],\n    ['True', true],\n  ]\n\n  for (const [code, expected] of testCases) {\n    const m = new Monty(code)\n    const result = m.start()\n    t.true(result instanceof MontyComplete)\n    t.deepEqual((result as MontyComplete).output, expected)\n  }\n})\n\n// =============================================================================\n// start() returns MontySnapshot tests (callable names go through FunctionCall)\n// =============================================================================\n\ntest('start with external function returns progress', (t) => {\n  const m = new Monty('func()')\n  const result = m.start()\n  t.true(result instanceof MontySnapshot)\n  const snapshot = result as MontySnapshot\n  t.is(snapshot.scriptName, 'main.py')\n  t.is(snapshot.functionName, 'func')\n  t.deepEqual(snapshot.args, [])\n  t.deepEqual(snapshot.kwargs, {})\n})\n\ntest('start custom script name', (t) => {\n  const m = new Monty('func()', { scriptName: 'custom.py' })\n  const result = m.start()\n  t.true(result instanceof MontySnapshot)\n  t.is((result as MontySnapshot).scriptName, 'custom.py')\n})\n\ntest('start progress with args', (t) => {\n  const m = new Monty('func(1, 2, 3)')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  t.is(snapshot.functionName, 'func')\n  t.deepEqual(snapshot.args, [1, 2, 3])\n  t.deepEqual(snapshot.kwargs, {})\n})\n\ntest('start progress with kwargs', (t) => {\n  const m = new Monty('func(a=1, b=\"two\")')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  t.is(snapshot.functionName, 'func')\n  t.deepEqual(snapshot.args, [])\n  t.deepEqual(snapshot.kwargs, { a: 1, b: 'two' })\n})\n\ntest('start progress with mixed args kwargs', (t) => {\n  const m = new Monty('func(1, 2, x=\"hello\", y=True)')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  t.is(snapshot.functionName, 'func')\n  t.deepEqual(snapshot.args, [1, 2])\n  t.deepEqual(snapshot.kwargs, { x: 'hello', y: true })\n})\n\n// =============================================================================\n// start() returns MontyNameLookup tests (non-callable name resolution)\n// =============================================================================\n\ntest('start with unknown name returns name lookup', (t) => {\n  const m = new Monty('x = foo; x')\n  const result = m.start()\n  t.true(result instanceof MontyNameLookup)\n  const lookup = result as MontyNameLookup\n  t.is(lookup.scriptName, 'main.py')\n  t.is(lookup.variableName, 'foo')\n})\n\ntest('name lookup resume with value completes', (t) => {\n  const m = new Monty('x = foo; x')\n  const result = m.start()\n  t.true(result instanceof MontyNameLookup)\n  const lookup = result as MontyNameLookup\n  t.is(lookup.variableName, 'foo')\n\n  const complete = lookup.resume({ value: 42 })\n  t.true(complete instanceof MontyComplete)\n  t.is((complete as MontyComplete).output, 42)\n})\n\ntest('name lookup resume without value raises NameError', (t) => {\n  const m = new Monty('x = foo; x')\n  const result = m.start()\n  t.true(result instanceof MontyNameLookup)\n  const lookup = result as MontyNameLookup\n\n  const error = t.throws(() => lookup.resume(), {\n    instanceOf: MontyRuntimeError,\n  })\n  t.true(error.message.includes('NameError'))\n  t.true(error.message.includes('foo'))\n})\n\ntest('name lookup custom script name', (t) => {\n  const m = new Monty('x = foo; x', { scriptName: 'custom.py' })\n  const result = m.start()\n  t.true(result instanceof MontyNameLookup)\n  t.is((result as MontyNameLookup).scriptName, 'custom.py')\n})\n\ntest('name lookup resume cannot be called twice', (t) => {\n  const m = new Monty('x = foo; x')\n  const lookup = m.start() as MontyNameLookup\n\n  // First resume succeeds\n  lookup.resume({ value: 42 })\n\n  // Second resume should fail\n  const error = t.throws(() => lookup.resume({ value: 99 }))\n  t.true(error?.message.includes('already'))\n})\n\ntest('name lookup resolves to function, then function call yields snapshot', (t) => {\n  // Assign an external function to x via name lookup, then call x()\n  const m = new Monty('x = foobar; x()')\n  const lookup = m.start()\n  t.true(lookup instanceof MontyNameLookup)\n  t.is((lookup as MontyNameLookup).variableName, 'foobar')\n\n  // Provide a function — JS functions convert to MontyObject::Function\n  function notFoobar(): unknown {\n    return 42\n  }\n  const snapshot = (lookup as MontyNameLookup).resume({ value: notFoobar })\n  t.true(snapshot instanceof MontySnapshot)\n  // Function name comes from the JS function's name, not the variable\n  t.is((snapshot as MontySnapshot).functionName, 'notFoobar')\n\n  const result = (snapshot as MontySnapshot).resume({ returnValue: 99 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 99)\n})\n\n// =============================================================================\n// resume() tests\n// =============================================================================\n\ntest('progress resume returns complete', (t) => {\n  const m = new Monty('func()')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  t.is(snapshot.functionName, 'func')\n  t.deepEqual(snapshot.args, [])\n  t.deepEqual(snapshot.kwargs, {})\n\n  const result = snapshot.resume({ returnValue: 42 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 42)\n})\n\ntest('resume with none', (t) => {\n  const m = new Monty('func()')\n  const snapshot = m.start() as MontySnapshot\n\n  const result = snapshot.resume({ returnValue: null })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, null)\n})\n\ntest('resume complex return value', (t) => {\n  const m = new Monty('func()')\n  const snapshot = m.start() as MontySnapshot\n\n  const complexValue = { a: [1, 2, 3], b: { nested: true } }\n  const result = snapshot.resume({ returnValue: complexValue })\n  t.true(result instanceof MontyComplete)\n  // JS objects become Maps in Python (and come back as Maps)\n  const output = (result as MontyComplete).output as Map<string, unknown>\n  t.true(output instanceof Map)\n  t.deepEqual(output.get('a'), [1, 2, 3])\n  const nestedMap = output.get('b') as Map<string, unknown>\n  t.true(nestedMap instanceof Map)\n  t.is(nestedMap.get('nested'), true)\n})\n\n// =============================================================================\n// Multiple external function calls tests\n// =============================================================================\n\ntest('multiple external calls', (t) => {\n  const m = new Monty('a() + b()')\n\n  // First call\n  let progress: MontySnapshot | MontyNameLookup | MontyComplete = m.start()\n  t.true(progress instanceof MontySnapshot)\n  t.is((progress as MontySnapshot).functionName, 'a')\n\n  // Resume with first return value\n  progress = (progress as MontySnapshot).resume({ returnValue: 10 })\n  t.true(progress instanceof MontySnapshot)\n  t.is((progress as MontySnapshot).functionName, 'b')\n\n  // Resume with second return value\n  const result = (progress as MontySnapshot).resume({ returnValue: 5 })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 15)\n})\n\ntest('chain of external calls', (t) => {\n  const m = new Monty('c() + c() + c()')\n\n  let callCount = 0\n  let progress: MontySnapshot | MontyNameLookup | MontyComplete = m.start()\n\n  while (progress instanceof MontySnapshot) {\n    t.is(progress.functionName, 'c')\n    callCount += 1\n    progress = progress.resume({ returnValue: callCount })\n  }\n\n  t.true(progress instanceof MontyComplete)\n  t.is((progress as MontyComplete).output, 6) // 1 + 2 + 3\n  t.is(callCount, 3)\n})\n\n// =============================================================================\n// start() with options tests\n// =============================================================================\n\ntest('start with inputs', (t) => {\n  const m = new Monty('process(x)', { inputs: ['x'] })\n  const progress = m.start({ inputs: { x: 100 } })\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n  t.is(snapshot.functionName, 'process')\n  t.deepEqual(snapshot.args, [100])\n})\n\ntest('start with limits', (t) => {\n  const m = new Monty('1 + 2')\n  const limits: ResourceLimits = { maxAllocations: 1000 }\n  const result = m.start({ limits })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, 3)\n})\n\n// =============================================================================\n// resume() cannot be called twice tests\n// =============================================================================\n\ntest('resume cannot be called twice', (t) => {\n  const m = new Monty('func()')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n\n  // First resume succeeds\n  snapshot.resume({ returnValue: 1 })\n\n  // Second resume should fail\n  const error = t.throws(() => snapshot.resume({ returnValue: 2 }))\n  t.true(error?.message.includes('already'))\n})\n\n// =============================================================================\n// resume() with exception tests\n// =============================================================================\n\ntest('resume with exception caught', (t) => {\n  const code = `\ntry:\n    result = external_func()\nexcept ValueError:\n    caught = True\ncaught\n`\n  const m = new Monty(code)\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n\n  // Resume with an exception using keyword argument\n  const result = snapshot.resume({ exception: { type: 'ValueError', message: 'test error' } })\n  t.true(result instanceof MontyComplete)\n  t.is((result as MontyComplete).output, true)\n})\n\ntest('resume exception propagates uncaught', (t) => {\n  const m = new Monty('external_func()')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n\n  // Resume with an exception that won't be caught - wrapped in MontyRuntimeError\n  const error = t.throws(() => snapshot.resume({ exception: { type: 'ValueError', message: 'uncaught error' } }), {\n    instanceOf: MontyRuntimeError,\n  })\n  t.true(error.message.includes('ValueError'))\n  t.true(error.message.includes('uncaught error'))\n})\n\ntest('resume exception in nested try', (t) => {\n  const code = `\nouter_caught = False\nfinally_ran = False\ntry:\n    try:\n        external_func()\n    except TypeError:\n        pass  # Won't catch ValueError\n    finally:\n        finally_ran = True\nexcept ValueError:\n    outer_caught = True\n(outer_caught, finally_ran)\n`\n  const m = new Monty(code)\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n\n  const result = snapshot.resume({ exception: { type: 'ValueError', message: 'propagates to outer' } })\n  t.true(result instanceof MontyComplete)\n  const output = (result as MontyComplete).output\n  t.true(Array.isArray(output))\n  t.is(output[0], true) // outer_caught\n  t.is(output[1], true) // finally_ran\n})\n\n// =============================================================================\n// Invalid resume args tests\n// =============================================================================\n\ntest('invalid resume args', (t) => {\n  const m = new Monty('func()')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const snapshot = progress as MontySnapshot\n\n  // Neither provided\n  const error = t.throws(() => snapshot.resume({} as ResumeOptions))\n  t.true(error?.message.includes('returnValue or exception'))\n})\n\n// =============================================================================\n// Monty instance reuse tests\n// =============================================================================\n\ntest('start can reuse monty instance', (t) => {\n  const m = new Monty('func(x)', { inputs: ['x'] })\n\n  // First run\n  const progress1 = m.start({ inputs: { x: 1 } })\n  t.true(progress1 instanceof MontySnapshot)\n  t.deepEqual((progress1 as MontySnapshot).args, [1])\n  const result1 = (progress1 as MontySnapshot).resume({ returnValue: 10 })\n  t.true(result1 instanceof MontyComplete)\n  t.is((result1 as MontyComplete).output, 10)\n\n  // Second run with different input\n  const progress2 = m.start({ inputs: { x: 2 } })\n  t.true(progress2 instanceof MontySnapshot)\n  t.deepEqual((progress2 as MontySnapshot).args, [2])\n  const result2 = (progress2 as MontySnapshot).resume({ returnValue: 20 })\n  t.true(result2 instanceof MontyComplete)\n  t.is((result2 as MontyComplete).output, 20)\n})\n\n// =============================================================================\n// OS call handling in start() tests\n// =============================================================================\n\ntest('os.environ via start() throws NotImplementedError instead of panicking', (t) => {\n  const m = new Monty('import os\\nx = os.environ')\n  const error = t.throws(() => m.start(), { instanceOf: MontyRuntimeError })\n  t.is(error.exception.typeName, 'NotImplementedError')\n  t.is(error.exception.message, \"OS function 'os.environ' not implemented\")\n})\n\ntest('os.getenv via start() throws NotImplementedError instead of panicking', (t) => {\n  const m = new Monty(\"import os\\nx = os.getenv('HOME')\")\n  const error = t.throws(() => m.start(), { instanceOf: MontyRuntimeError })\n  t.is(error.exception.typeName, 'NotImplementedError')\n  t.is(error.exception.message, \"OS function 'os.getenv' not implemented\")\n})\n\n// =============================================================================\n// repr() tests\n// =============================================================================\n\ntest('name lookup repr', (t) => {\n  const m = new Monty('x = foo; x')\n  const progress = m.start()\n  t.true(progress instanceof MontyNameLookup)\n  const repr = (progress as MontyNameLookup).repr()\n  t.true(repr.includes('MontyNameLookup'))\n  t.true(repr.includes('foo'))\n})\n\ntest('progress repr', (t) => {\n  const m = new Monty('func(1, x=2)')\n  const progress = m.start()\n  t.true(progress instanceof MontySnapshot)\n  const repr = (progress as MontySnapshot).repr()\n  t.true(repr.includes('MontySnapshot'))\n  t.true(repr.includes('func'))\n})\n\ntest('complete repr', (t) => {\n  const m = new Monty('42')\n  const result = m.start()\n  t.true(result instanceof MontyComplete)\n  const repr = (result as MontyComplete).repr()\n  t.true(repr.includes('MontyComplete'))\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/type_check.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty, MontyTypingError } from '../wrapper'\n\n// =============================================================================\n// typeCheck() tests\n// =============================================================================\n\ntest('type check no errors', (t) => {\n  const m = new Monty('x = 1')\n  t.notThrows(() => m.typeCheck())\n})\n\ntest('type check with errors', (t) => {\n  const m = new Monty('\"hello\" + 1')\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  t.true(error.message.includes('unsupported-operator'))\n})\n\ntest('type check function return type', (t) => {\n  const code = `\ndef foo() -> int:\n    return \"not an int\"\n`\n  const m = new Monty(code)\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  t.true(error.message.includes('invalid-return-type'))\n})\n\ntest('type check undefined variable', (t) => {\n  const m = new Monty('print(undefined_var)')\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  t.true(error.message.includes('unresolved-reference'))\n})\n\ntest('type check valid function', (t) => {\n  const code = `\ndef add(a: int, b: int) -> int:\n    return a + b\n\nadd(1, 2)\n`\n  const m = new Monty(code)\n  t.notThrows(() => m.typeCheck())\n})\n\ntest('type check with prefix code', (t) => {\n  const m = new Monty('result = x + 1')\n  // Without prefix, x is undefined\n  t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  // With prefix declaring x as a variable, it should pass\n  t.notThrows(() => m.typeCheck('x = 0'))\n})\n\n// =============================================================================\n// Constructor type_check parameter tests\n// =============================================================================\n\ntest('constructor type check default false', (t) => {\n  // This should NOT raise during construction (typeCheck=false is default)\n  const m = new Monty('\"hello\" + 1')\n  // But we can still call typeCheck() manually later\n  t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n})\n\ntest('constructor type check explicit true', (t) => {\n  t.throws(() => new Monty('\"hello\" + 1', { typeCheck: true }), { instanceOf: MontyTypingError })\n})\n\ntest('constructor type check explicit false', (t) => {\n  // This should NOT raise during construction\n  const m = new Monty('\"hello\" + 1', { typeCheck: false })\n  // But we can still call typeCheck() manually later\n  t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n})\n\ntest('constructor default allows run with inputs', (t) => {\n  // Code with undefined variable - type checking would fail\n  const m = new Monty('x + 1', { inputs: ['x'] })\n  // But runtime works fine with the input provided\n  const result = m.run({ inputs: { x: 5 } })\n  t.is(result, 6)\n})\n\ntest('constructor type check prefix code', (t) => {\n  // Without prefix, this would fail type checking (x is undefined)\n  // Use assignment to define x, not just type annotation\n  t.notThrows(() => new Monty('result = x + 1', { typeCheck: true, typeCheckPrefixCode: 'x = 0' }))\n})\n\ntest('constructor type check prefix code with external function', (t) => {\n  // Define fetch as a function that takes a string and returns a string\n  const prefix = `\ndef fetch(url: str) -> str:\n    return ''\n`\n  t.notThrows(\n    () =>\n      new Monty('result = fetch(\"https://example.com\")', {\n        typeCheck: true,\n        typeCheckPrefixCode: prefix,\n      }),\n  )\n})\n\ntest('constructor type check prefix code invalid', (t) => {\n  // Prefix defines x as str, but code tries to use it with int addition\n  t.throws(\n    () =>\n      new Monty('result: int = x + 1', {\n        typeCheck: true,\n        typeCheckPrefixCode: 'x = \"hello\"',\n      }),\n    { instanceOf: MontyTypingError },\n  )\n})\n\n// =============================================================================\n// MontyTypingError tests\n// =============================================================================\n\ntest('monty typing error is monty error subclass', (t) => {\n  const m = new Monty('\"hello\" + 1')\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  t.true(error instanceof Error)\n})\n\ntest('monty typing error displayDiagnostics', (t) => {\n  const m = new Monty('\"hello\" + 1')\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  // displayDiagnostics() returns rich diagnostics, display('msg') returns the raw message\n  t.is(error.message, `TypeError: ${error.display('msg')}`)\n})\n\ntest('monty typing error displayDiagnostics concise format', (t) => {\n  const m = new Monty('\"hello\" + 1')\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  const concise = error.displayDiagnostics('concise')\n  t.true(concise.includes('error[unsupported-operator]'))\n})\n\ntest('monty typing error inherits base display formats', (t) => {\n  const m = new Monty('\"hello\" + 1')\n  const error = t.throws(() => m.typeCheck(), { instanceOf: MontyTypingError })\n  t.is(error.display('msg'), error.exception.message)\n  t.true(error.display('type-msg').startsWith('TypeError:'))\n})\n"
  },
  {
    "path": "crates/monty-js/__test__/types.spec.ts",
    "content": "import test from 'ava'\n\nimport { Monty } from '../wrapper'\nimport { Buffer } from 'node:buffer'\n\n// =============================================================================\n// None tests\n// =============================================================================\n\ntest('none input', (t) => {\n  const m = new Monty('x is None', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: null } }), true)\n})\n\ntest('none output', (t) => {\n  const m = new Monty('None')\n  t.is(m.run(), null)\n})\n\n// =============================================================================\n// Bool tests\n// =============================================================================\n\ntest('bool true', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: true } })\n  t.is(result, true)\n})\n\ntest('bool false', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: false } })\n  t.is(result, false)\n})\n\n// =============================================================================\n// Number tests\n// =============================================================================\n\ntest('int', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: 42 } }), 42)\n  t.is(m.run({ inputs: { x: -100 } }), -100)\n  t.is(m.run({ inputs: { x: 0 } }), 0)\n})\n\ntest('float', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: 3.14 } }), 3.14)\n  t.is(m.run({ inputs: { x: -2.5 } }), -2.5)\n  t.is(m.run({ inputs: { x: 0.0 } }), 0.0)\n})\n\n// =============================================================================\n// String tests\n// =============================================================================\n\ntest('string', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: 'hello' } }), 'hello')\n  t.is(m.run({ inputs: { x: '' } }), '')\n  t.is(m.run({ inputs: { x: 'unicode: éè' } }), 'unicode: éè')\n})\n\n// =============================================================================\n// Bytes tests\n// =============================================================================\n\ntest('bytes', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: Buffer.from('hello') } })\n  t.true(Buffer.isBuffer(result))\n  t.deepEqual([...result], [104, 101, 108, 108, 111])\n})\n\ntest('bytes empty', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: Buffer.from([]) } })\n  t.true(Buffer.isBuffer(result))\n  t.deepEqual([...result], [])\n})\n\ntest('bytes result', (t) => {\n  const m = new Monty('b\"hello\"')\n  const result = m.run()\n  t.true(Buffer.isBuffer(result))\n  t.deepEqual([...result], [104, 101, 108, 108, 111])\n})\n\n// =============================================================================\n// List tests\n// =============================================================================\n\ntest('list', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  t.deepEqual(m.run({ inputs: { x: [1, 2, 3] } }), [1, 2, 3])\n  t.deepEqual(m.run({ inputs: { x: [] } }), [])\n  t.deepEqual(m.run({ inputs: { x: ['a', 'b'] } }), ['a', 'b'])\n})\n\ntest('list output', (t) => {\n  const m = new Monty('[1, 2, 3]')\n  t.deepEqual(m.run(), [1, 2, 3])\n})\n\n// =============================================================================\n// Tuple tests\n// =============================================================================\n\ntest('tuple', (t) => {\n  const m = new Monty('(1, 2, 3)')\n  const result = m.run()\n  // Tuples are returned as arrays with a __tuple__ marker property\n  t.true(Array.isArray(result))\n  t.deepEqual([...result], [1, 2, 3])\n  t.is(result.__tuple__, true)\n})\n\ntest('tuple empty', (t) => {\n  const m = new Monty('()')\n  const result = m.run()\n  t.true(Array.isArray(result))\n  t.deepEqual([...result], [])\n  t.is(result.__tuple__, true)\n})\n\n// =============================================================================\n// Dict tests\n// =============================================================================\n\ntest('dict', (t) => {\n  const m = new Monty('{\"a\": 1, \"b\": 2}')\n  const result = m.run()\n  // Dicts are returned as native JS Map (preserves key types and insertion order)\n  t.true(result instanceof Map)\n  t.is(result.get('a'), 1)\n  t.is(result.get('b'), 2)\n  t.is(result.size, 2)\n})\n\ntest('dict empty', (t) => {\n  const m = new Monty('{}')\n  const result = m.run()\n  t.true(result instanceof Map)\n  t.is(result.size, 0)\n})\n\n// =============================================================================\n// Set tests\n// =============================================================================\n\ntest('set', (t) => {\n  const m = new Monty('{1, 2, 3}')\n  const result = m.run()\n  t.deepEqual(result, new Set([1, 2, 3]))\n})\n\ntest('set empty', (t) => {\n  const m = new Monty('set()')\n  const result = m.run()\n  t.deepEqual(result, new Set())\n})\n\n// =============================================================================\n// Frozenset tests\n// =============================================================================\n\ntest('frozenset', (t) => {\n  const m = new Monty('frozenset([1, 2, 3])')\n  const result = m.run()\n  // FrozenSet is returned as a native JS Set (no frozen equivalent in JS)\n  t.true(result instanceof Set)\n  t.deepEqual(result, new Set([1, 2, 3]))\n})\n\ntest('frozenset empty', (t) => {\n  const m = new Monty('frozenset()')\n  const result = m.run()\n  t.deepEqual(result, new Set())\n})\n\n// =============================================================================\n// Ellipsis tests\n// =============================================================================\n\ntest('ellipsis input', (t) => {\n  // In JS we represent ellipsis as an object with __monty_type__: 'Ellipsis'\n  const m = new Monty('x is ...', { inputs: ['x'] })\n  t.is(m.run({ inputs: { x: { __monty_type__: 'Ellipsis' } } }), true)\n})\n\ntest('ellipsis output', (t) => {\n  const m = new Monty('...')\n  const result = m.run()\n  t.deepEqual(result, { __monty_type__: 'Ellipsis' })\n})\n\n// =============================================================================\n// Nested collection tests\n// =============================================================================\n\ntest('nested list', (t) => {\n  const m = new Monty('x', { inputs: ['x'] })\n  const nested = [\n    [1, 2],\n    [3, [4, 5]],\n  ]\n  t.deepEqual(m.run({ inputs: { x: nested } }), [\n    [1, 2],\n    [3, [4, 5]],\n  ])\n})\n\ntest('nested dict', (t) => {\n  const m = new Monty('{\"list\": [1, 2], \"nested\": {\"a\": 1}}')\n  const result = m.run()\n  // Dicts are returned as native JS Map\n  t.true(result instanceof Map)\n  t.deepEqual(result.get('list'), [1, 2])\n  const nested = result.get('nested')\n  t.true(nested instanceof Map)\n  t.is(nested.get('a'), 1)\n})\n\ntest('mixed nested', (t) => {\n  const m = new Monty('{\"list\": [1, 2], \"tuple\": (3, 4), \"nested\": {\"set\": {5, 6}}}')\n  const result = m.run()\n  t.true(result instanceof Map)\n  t.deepEqual(result.get('list'), [1, 2])\n  const tuple = result.get('tuple')\n  t.true(Array.isArray(tuple))\n  t.is(tuple.__tuple__, true)\n  t.deepEqual([...tuple], [3, 4])\n  const nested = result.get('nested')\n  t.true(nested instanceof Map)\n  t.true(nested.get('set') instanceof Set)\n})\n\ntest('nested set in list', (t) => {\n  const m = new Monty('[{1, 2}, {3, 4}]')\n  const result = m.run()\n  t.true(Array.isArray(result))\n  t.is(result.length, 2)\n  t.true(result[0] instanceof Set)\n  t.true(result[1] instanceof Set)\n  t.deepEqual(result[0], new Set([1, 2]))\n  t.deepEqual(result[1], new Set([3, 4]))\n})\n\ntest('nested bytes in dict', (t) => {\n  const m = new Monty('{\"data\": b\"abc\"}')\n  const result = m.run()\n  t.true(result instanceof Map)\n  const data = result.get('data')\n  t.true(Buffer.isBuffer(data))\n  t.deepEqual([...data], [97, 98, 99])\n})\n\ntest('tuple containing set', (t) => {\n  const m = new Monty('({1, 2}, \"hello\")')\n  const result = m.run()\n  t.true(Array.isArray(result))\n  t.is(result.__tuple__, true)\n  t.true(result[0] instanceof Set)\n  t.deepEqual(result[0], new Set([1, 2]))\n  t.is(result[1], 'hello')\n})\n\n// =============================================================================\n// BigInt tests\n// =============================================================================\n\ntest('bigint input', (t) => {\n  const big = 2n ** 100n\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: big } })\n  t.is(result, big)\n})\n\ntest('bigint output', (t) => {\n  const m = new Monty('2**100')\n  const result = m.run()\n  t.is(result, 2n ** 100n)\n})\n\ntest('bigint negative input', (t) => {\n  const bigNeg = -(2n ** 100n)\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: bigNeg } })\n  t.is(result, bigNeg)\n})\n\ntest('int overflow to bigint', (t) => {\n  const maxI64 = 9223372036854775807n\n  const m = new Monty('x + 1', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: maxI64 } })\n  t.is(result, maxI64 + 1n)\n})\n\ntest('bigint arithmetic', (t) => {\n  const big = 2n ** 100n\n  const m = new Monty('x * 2 + y', { inputs: ['x', 'y'] })\n  const result = m.run({ inputs: { x: big, y: big } })\n  t.is(result, big * 2n + big)\n})\n\ntest('bigint comparison', (t) => {\n  const big = 2n ** 100n\n  const m = new Monty('x > y', { inputs: ['x', 'y'] })\n  t.is(m.run({ inputs: { x: big, y: 42 } }), true)\n  t.is(m.run({ inputs: { x: 42, y: big } }), false)\n})\n\ntest('bigint in collection', (t) => {\n  const big = 2n ** 100n\n  const m = new Monty('x', { inputs: ['x'] })\n  const result = m.run({ inputs: { x: [big, 42, big * 2n] } })\n  t.deepEqual(result, [big, 42, big * 2n])\n})\n"
  },
  {
    "path": "crates/monty-js/build.rs",
    "content": "use std::{env, fs, path::Path, process::Command};\n\n/// Build script that sets up napi bindings and syncs the package.json version\n/// with the Cargo workspace version.\n///\n/// Cargo sets `CARGO_PKG_VERSION` in the environment when executing build scripts,\n/// so we use that as the single source of truth. If package.json has a different\n/// version, we update it in place.\nfn main() {\n    // Re-run when package.json changes so we can re-check the version.\n    println!(\"cargo:rerun-if-changed=package.json\");\n    sync_package_json_version();\n    napi_build::setup();\n}\n\n/// Read the Cargo package version and update package.json if the version differs.\n///\n/// Uses the runtime `CARGO_PKG_VERSION` env var (not `env!()`) so that the build\n/// script picks up version changes without needing to be recompiled.\nfn sync_package_json_version() {\n    let cargo_version = env::var(\"CARGO_PKG_VERSION\").expect(\"CARGO_PKG_VERSION not set\");\n    let package_json_path = Path::new(\"package.json\");\n\n    let contents = fs::read_to_string(package_json_path).expect(\"failed to read package.json\");\n\n    // Replace the top-level \"version\" field. We match lines starting with\n    // `  \"version\":` which is the standard prettier-formatted location.\n    let expected = format!(\"  \\\"version\\\": \\\"{cargo_version}\\\",\");\n    let mut result = String::with_capacity(contents.len());\n    let mut changed = false;\n\n    for line in contents.lines() {\n        // Only match the top-level \"version\" field (exactly 2-space indent),\n        // not nested ones like scripts.version (4-space indent).\n        if !changed && line.starts_with(\"  \\\"version\\\"\") {\n            // version unchanged, exit early\n            if line == expected {\n                return;\n            }\n            result.push_str(&expected);\n            changed = true;\n        } else {\n            result.push_str(line);\n        }\n        result.push('\\n');\n    }\n\n    if !changed {\n        return;\n    }\n\n    eprintln!(\"Updating package.json version to {cargo_version}\");\n    fs::write(package_json_path, &result).expect(\"failed to write package.json\");\n\n    // Sync package-lock.json to match the updated version.\n    let status = Command::new(\"npm\")\n        .args([\"install\", \"--package-lock-only\"])\n        .status()\n        .expect(\"failed to run npm\");\n    assert!(status.success(), \"npm install --package-lock-only failed\");\n}\n"
  },
  {
    "path": "crates/monty-js/index-header.d.ts",
    "content": "// index-header.d.ts - header will be written into index.d.ts on build\n\ntype JsMontyObject = any\n"
  },
  {
    "path": "crates/monty-js/package.json",
    "content": "{\n  \"name\": \"@pydantic/monty\",\n  \"version\": \"0.0.8\",\n  \"type\": \"module\",\n  \"description\": \"Sandboxed Python interpreter for JavaScript/TypeScript\",\n  \"main\": \"wrapper.js\",\n  \"types\": \"wrapper.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./wrapper.d.ts\",\n      \"default\": \"./wrapper.js\"\n    }\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/pydantic/monty\"\n  },\n  \"license\": \"MIT\",\n  \"browser\": \"browser.js\",\n  \"files\": [\n    \"index.d.ts\",\n    \"index.js\",\n    \"wrapper.js\",\n    \"wrapper.d.ts\",\n    \"browser.js\"\n  ],\n  \"napi\": {\n    \"binaryName\": \"monty\",\n    \"targets\": [\n      \"x86_64-pc-windows-msvc\",\n      \"x86_64-apple-darwin\",\n      \"x86_64-unknown-linux-gnu\",\n      \"aarch64-apple-darwin\",\n      \"aarch64-unknown-linux-gnu\",\n      \"wasm32-wasip1-threads\"\n    ],\n    \"dtsHeaderFile\": \"./index-header.d.ts\"\n  },\n  \"engines\": {\n    \"node\": \">= 6.14.2 < 7 || >= 8.11.2 < 9 || >= 9.11.0 < 10 || >= 10.0.0\"\n  },\n  \"publishConfig\": {\n    \"registry\": \"https://registry.npmjs.org/\",\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"artifacts\": \"napi artifacts\",\n    \"bench\": \"node --import @oxc-node/core/register benchmark/bench.ts\",\n    \"build\": \"run-s build:napi build:ts\",\n    \"build:debug\": \"run-s build:napi:debug build:ts\",\n    \"build:napi\": \"napi build --platform --release --esm\",\n    \"build:napi:debug\": \"napi build --platform --esm\",\n    \"build:ts\": \"tsc\",\n    \"create-npm-dirs\": \"napi create-npm-dirs\",\n    \"format\": \"run-p format:prettier format:rs format:toml\",\n    \"format:prettier\": \"prettier . -w\",\n    \"format:toml\": \"taplo format\",\n    \"format:rs\": \"cargo fmt\",\n    \"lint\": \"oxlint .\",\n    \"prepublishOnly\": \"napi prepublish -t npm\",\n    \"test\": \"ava\",\n    \"smoke-test\": \"bash scripts/smoke-test.sh\",\n    \"preversion\": \"napi build --platform && git add .\",\n    \"version\": \"napi version\"\n  },\n  \"devDependencies\": {\n    \"@emnapi/core\": \"^1.5.0\",\n    \"@emnapi/runtime\": \"^1.5.0\",\n    \"@napi-rs/cli\": \"^3.2.0\",\n    \"@oxc-node/core\": \"^0.0.35\",\n    \"@taplo/cli\": \"^0.7.0\",\n    \"@tybys/wasm-util\": \"^0.10.0\",\n    \"@types/node\": \"^25.0.9\",\n    \"ava\": \"^6.4.1\",\n    \"chalk\": \"^5.6.2\",\n    \"npm-run-all2\": \"^8.0.4\",\n    \"oxlint\": \"^1.14.0\",\n    \"prettier\": \"^3.6.2\",\n    \"tinybench\": \"^6.0.0\",\n    \"typescript\": \"^5.9.2\"\n  },\n  \"ava\": {\n    \"extensions\": {\n      \"ts\": \"module\"\n    },\n    \"timeout\": \"2m\",\n    \"workerThreads\": false,\n    \"nodeArguments\": [\n      \"--import\",\n      \"@oxc-node/core/register\"\n    ]\n  },\n  \"prettier\": {\n    \"printWidth\": 120,\n    \"semi\": false,\n    \"trailingComma\": \"all\",\n    \"singleQuote\": true,\n    \"arrowParens\": \"always\"\n  }\n}\n"
  },
  {
    "path": "crates/monty-js/scripts/smoke-test.sh",
    "content": "#!/bin/bash\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"$SCRIPT_DIR/..\" && pwd)\"\n\ncd \"$ROOT_DIR\"\n\necho \"=== Building package ===\"\nnpm run build\n\n# Detect current platform\nNODE_FILE=$(ls monty.*.node 2>/dev/null | head -1)\nif [ -z \"$NODE_FILE\" ]; then\n    echo \"Error: No .node file found after build\"\n    exit 1\nfi\n\n# Extract platform from filename (e.g., monty.darwin-arm64.node -> darwin-arm64)\nPLATFORM=$(echo \"$NODE_FILE\" | sed 's/monty\\.\\(.*\\)\\.node/\\1/')\necho \"Detected platform: $PLATFORM\"\n\necho \"=== Setting up platform packages ===\"\nnpm run create-npm-dirs\n\n# Copy binary to platform package directory (simulates napi artifacts)\nPLATFORM_DIR=\"npm/$PLATFORM\"\nif [ ! -d \"$PLATFORM_DIR\" ]; then\n    echo \"Error: Platform directory $PLATFORM_DIR not found\"\n    exit 1\nfi\ncp \"$NODE_FILE\" \"$PLATFORM_DIR/\"\n\n# Add optionalDependencies to main package.json (without publishing)\nnpx napi prepublish -t npm --skip-optional-publish\n\necho \"=== Creating platform package tgz ===\"\ncd \"$PLATFORM_DIR\"\nPLATFORM_TGZ=$(npm pack 2>/dev/null)\nmv \"$PLATFORM_TGZ\" \"$ROOT_DIR/\"\ncd \"$ROOT_DIR\"\necho \"Created: $PLATFORM_TGZ\"\n\necho \"=== Creating main package tgz ===\"\nMAIN_TGZ=$(npm pack 2>/dev/null)\necho \"Created: $MAIN_TGZ\"\n\necho \"=== Installing in smoke-test ===\"\ncd \"$ROOT_DIR/smoke-test\"\nrm -rf node_modules package-lock.json\n\n# Install platform package first, then main package\nnpm install \"../$PLATFORM_TGZ\" --force\nnpm install \"../$MAIN_TGZ\" --force\n\necho \"=== Type checking ===\"\nnpm run type-check\n\necho \"=== Running smoke tests ===\"\nnpm test\n\necho \"=== Cleaning up ===\"\ncd \"$ROOT_DIR\"\nrm -f \"$MAIN_TGZ\" \"$PLATFORM_TGZ\"\nrm -rf npm/\n# Remove optionalDependencies added by napi prepublish (keeps other package.json changes)\nnpm pkg delete optionalDependencies 2>/dev/null || true\n\necho \"=== Smoke test passed! ===\"\n"
  },
  {
    "path": "crates/monty-js/smoke-test/.gitignore",
    "content": "node_modules/\n*.tgz\npackage-lock.json\n"
  },
  {
    "path": "crates/monty-js/smoke-test/package.json",
    "content": "{\n  \"name\": \"monty-smoke-test\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"type-check\": \"tsc --noEmit\",\n    \"test\": \"node --experimental-strip-types test.ts\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.9.2\"\n  },\n  \"dependencies\": {\n    \"@pydantic/monty\": \"file:../pydantic-monty-1.0.0.tgz\",\n    \"@pydantic/monty-darwin-arm64\": \"file:../pydantic-monty-darwin-arm64-1.0.0.tgz\"\n  }\n}\n"
  },
  {
    "path": "crates/monty-js/smoke-test/test.ts",
    "content": "import { Monty, MontySyntaxError, MontyRuntimeError, MontySnapshot, MontyComplete } from '@pydantic/monty'\n\nlet passed = 0\nlet failed = 0\n\nfunction assert(condition: boolean, message: string): void {\n  if (!condition) {\n    console.error(`FAIL: ${message}`)\n    failed++\n  } else {\n    console.log(`PASS: ${message}`)\n    passed++\n  }\n}\n\nfunction assertThrows<T extends Error>(fn: () => void, errorClass: new (...args: never[]) => T, message: string): void {\n  try {\n    fn()\n    console.error(`FAIL: ${message} - no error thrown`)\n    failed++\n  } catch (e) {\n    if (e instanceof errorClass) {\n      console.log(`PASS: ${message}`)\n      passed++\n    } else {\n      console.error(`FAIL: ${message} - wrong error type: ${(e as Error).constructor.name}`)\n      failed++\n    }\n  }\n}\n\nconsole.log('=== Basic Execution ===')\n\nconst m1 = new Monty('1 + 2')\nassert(m1.run() === 3, 'basic arithmetic')\n\nconst m2 = new Monty('10 * 5 - 3')\nassert(m2.run() === 47, 'complex arithmetic')\n\nconst m3 = new Monty('\"hello\" + \" \" + \"world\"')\nassert(m3.run() === 'hello world', 'string concatenation')\n\nconsole.log('\\n=== Constructor Options ===')\n\nconst m4 = new Monty('x + y', { inputs: ['x', 'y'] })\nassert(m4.inputs.length === 2, 'inputs array populated')\nassert(m4.inputs[0] === 'x', 'first input correct')\n\n// External functions are no longer declared in the constructor - they are resolved at runtime via start/resume\n\nconst m6 = new Monty('1', { scriptName: 'custom.py' })\nassert(m6.scriptName === 'custom.py', 'custom script name')\n\nconsole.log('\\n=== Inputs ===')\n\nconst m7 = new Monty('x * 2', { inputs: ['x'] })\nassert(m7.run({ inputs: { x: 5 } }) === 10, 'single input')\nassert(m7.run({ inputs: { x: -3 } }) === -6, 'negative input')\n\nconst m8 = new Monty('a + b + c', { inputs: ['a', 'b', 'c'] })\nassert(m8.run({ inputs: { a: 1, b: 2, c: 3 } }) === 6, 'multiple inputs')\n\nconsole.log('\\n=== Error Handling ===')\n\nassertThrows(() => new Monty('def'), MontySyntaxError, 'syntax error throws MontySyntaxError')\n\nassertThrows(() => new Monty('1/0').run(), MontyRuntimeError, 'division by zero throws MontyRuntimeError')\n\nassertThrows(\n  () => new Monty('raise ValueError(\"test\")').run(),\n  MontyRuntimeError,\n  'raise statement throws MontyRuntimeError',\n)\n\nconsole.log('\\n=== Error Properties ===')\n\ntry {\n  new Monty('raise ValueError(\"custom message\")').run()\n} catch (e) {\n  if (e instanceof MontyRuntimeError) {\n    assert(e.exception.typeName === 'ValueError', 'exception typeName correct')\n    assert(e.exception.message === 'custom message', 'exception message correct')\n    assert(e.display('msg') === 'custom message', 'display msg format')\n    assert(e.display('type-msg') === 'ValueError: custom message', 'display type-msg format')\n    const frames = e.traceback()\n    assert(Array.isArray(frames), 'traceback returns array')\n  }\n}\n\nconsole.log('\\n=== External Functions (start/resume) ===')\n\nconst m9 = new Monty('foo(42)')\nconst result9 = m9.start()\nassert(result9 instanceof MontySnapshot, 'start returns MontySnapshot')\nif (!(result9 instanceof MontySnapshot)) throw new Error('Expected MontySnapshot')\nassert(result9.functionName === 'foo', 'snapshot has correct function name')\nassert(result9.args[0] === 42, 'snapshot has correct args')\nassert(Object.keys(result9.kwargs).length === 0, 'snapshot has empty kwargs')\n\nconst complete1 = result9.resume({ returnValue: 'result' })\nassert(complete1 instanceof MontyComplete, 'resume returns MontyComplete')\nif (!(complete1 instanceof MontyComplete)) throw new Error('Expected MontyComplete')\nassert(complete1.output === 'result', 'complete has correct output')\n\nconsole.log('\\n=== External Functions with kwargs ===')\n\nconst m10 = new Monty('bar(1, 2, x=3, y=4)')\nconst result10 = m10.start()\nif (!(result10 instanceof MontySnapshot)) throw new Error('Expected MontySnapshot')\nassert(result10.args[0] === 1, 'positional arg 1')\nassert(result10.args[1] === 2, 'positional arg 2')\nassert(result10.kwargs['x'] === 3, 'kwarg x')\nassert(result10.kwargs['y'] === 4, 'kwarg y')\nresult10.resume({ returnValue: null })\n\nconsole.log('\\n=== Multiple External Calls ===')\n\nconst m11 = new Monty('a = get_a()\\nb = get_b()\\na + b')\nlet state: MontySnapshot | MontyComplete = m11.start()\n\nassert(state instanceof MontySnapshot, 'first call returns snapshot')\nassert((state as MontySnapshot).functionName === 'get_a', 'first function is get_a')\nstate = (state as MontySnapshot).resume({ returnValue: 10 })\n\nassert(state instanceof MontySnapshot, 'second call returns snapshot')\nassert((state as MontySnapshot).functionName === 'get_b', 'second function is get_b')\nstate = (state as MontySnapshot).resume({ returnValue: 20 })\n\nassert(state instanceof MontyComplete, 'final state is complete')\nassert((state as MontyComplete).output === 30, 'result is sum of external returns')\n\nconsole.log('\\n=== Serialization ===')\n\nconst m12 = new Monty('x + 1', { inputs: ['x'] })\nconst dumped = m12.dump()\nassert(dumped instanceof Buffer, 'dump returns Buffer')\nassert(dumped.length > 0, 'dump is not empty')\n\nconst loaded = Monty.load(dumped)\nassert(loaded.run({ inputs: { x: 10 } }) === 11, 'loaded instance works')\n\nconsole.log('\\n=== Snapshot Serialization ===')\n\nconst m13 = new Monty('ext(x) + 1', { inputs: ['x'] })\nconst snap = m13.start({ inputs: { x: 5 } }) as MontySnapshot\nconst snapDumped = snap.dump()\nassert(snapDumped instanceof Buffer, 'snapshot dump returns Buffer')\n\nconst snapLoaded = MontySnapshot.load(snapDumped)\nassert(snapLoaded.functionName === 'ext', 'loaded snapshot has function name')\nassert(snapLoaded.args[0] === 5, 'loaded snapshot has args')\n\nconst finalResult = snapLoaded.resume({ returnValue: 100 }) as MontyComplete\nassert(finalResult.output === 101, 'resumed loaded snapshot works')\n\nconsole.log('\\n=== repr() ===')\n\nconst m14 = new Monty('1 + 1')\nconst repr = m14.repr()\nassert(typeof repr === 'string', 'repr returns string')\nassert(repr.includes('Monty'), 'repr contains Monty')\n\nconsole.log('\\n=== Summary ===')\nconsole.log(`Passed: ${passed}`)\nconsole.log(`Failed: ${failed}`)\n\nif (failed > 0) {\n  process.exit(1)\n}\n\nconsole.log('\\nAll smoke tests passed!')\n"
  },
  {
    "path": "crates/monty-js/smoke-test/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"strict\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"test.ts\"]\n}\n"
  },
  {
    "path": "crates/monty-js/src/convert.rs",
    "content": "//! Type conversion between Monty's `MontyObject` and JavaScript values via napi-rs.\n//!\n//! This module provides bidirectional conversion using native napi-rs APIs:\n//! - `monty_to_js`: Convert Monty's `MontyObject` to a JavaScript value\n//! - `js_to_monty`: Convert a JavaScript value to Monty's `MontyObject`\n//!\n//! ## Type Mappings\n//!\n//! ### Native JS types (bidirectional):\n//! - `MontyObject::None` ↔ `null`\n//! - `MontyObject::Bool` ↔ `boolean`\n//! - `MontyObject::Int` ↔ `number` (if within safe integer range) or `BigInt`\n//! - `MontyObject::BigInt` ↔ `BigInt`\n//! - `MontyObject::Float` ↔ `number` (including `NaN`, `Infinity`, `-Infinity`)\n//! - `MontyObject::String` ↔ `string`\n//! - `MontyObject::Bytes` ↔ `Buffer` (Node.js)\n//! - `MontyObject::List` ↔ `Array`\n//! - `MontyObject::Dict` ↔ `Map` (preserves key types and insertion order)\n//! - `MontyObject::Set` ↔ `Set`\n//! - `MontyObject::FrozenSet` ↔ `Set` (JS has no frozen set)\n//!\n//! ### Marked JS types (with `__monty_type__` property):\n//! - `MontyObject::Ellipsis` → `{ __monty_type__: 'Ellipsis' }`\n//! - `MontyObject::Tuple` → `Array` with `__tuple__: true`\n//! - `MontyObject::Exception` → `{ __monty_type__: 'Exception', excType, message }`\n//! - `MontyObject::Type` → `{ __monty_type__: 'Type', value }`\n//! - `MontyObject::BuiltinFunction` → `{ __monty_type__: 'BuiltinFunction', value }`\n//! - `MontyObject::Dataclass` → `{ __monty_type__: 'Dataclass', name, fields, ... }`\n//! - `MontyObject::Repr` → plain `string`\n//! - `MontyObject::Cycle` → placeholder `string`\n\nuse std::collections::HashMap;\n\nuse monty::{DictPairs, ExcType, MontyObject};\nuse napi::bindgen_prelude::*;\nuse num_bigint::BigInt as NumBigInt;\n\n/// JavaScript safe integer range: -(2^53) to 2^53.\nconst JS_SAFE_INT_MIN: i64 = -(1_i64 << 53);\nconst JS_SAFE_INT_MAX: i64 = 1_i64 << 53;\n\n/// Wrapper for returning an unknown JS value from napi functions.\n///\n/// This allows `monty_to_js` to return dynamically typed JS values.\npub struct JsMontyObject<'env>(pub(crate) Unknown<'env>);\n\nimpl JsMontyObject<'_> {\n    /// Returns the raw napi value for use in low-level operations.\n    pub fn raw(&self) -> sys::napi_value {\n        self.0.raw()\n    }\n}\n\nimpl ToNapiValue for JsMontyObject<'_> {\n    unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> {\n        Unknown::to_napi_value(env, val.0)\n    }\n}\n\n/// Converts Monty's `MontyObject` to a JavaScript value using native napi-rs APIs.\n///\n/// This function creates native JS types where possible:\n/// - Numbers use JS `number` or `BigInt` depending on size\n/// - Dicts use native JS `Map` (preserves key types and insertion order)\n/// - Sets use native JS `Set`\n/// - Bytes use Node.js `Buffer`\n/// - Tuples use arrays with a `__tuple__` marker property\n///\n/// Types that don't have direct JS equivalents get marker properties to preserve\n/// type information for round-tripping.\npub fn monty_to_js<'e>(obj: &MontyObject, env: &'e Env) -> Result<JsMontyObject<'e>> {\n    let unknown = match obj {\n        MontyObject::None => create_js_null(env)?,\n        MontyObject::Ellipsis => create_js_ellipsis(env)?,\n        MontyObject::Bool(b) => create_js_bool(*b, env)?,\n        MontyObject::Int(i) => create_js_int(*i, env)?,\n        MontyObject::BigInt(bi) => create_js_bigint(bi, env)?,\n        MontyObject::Float(f) => env.create_double(*f)?.into_unknown(env)?,\n        MontyObject::String(s) => env.create_string(s)?.into_unknown(env)?,\n        MontyObject::Bytes(bytes) => create_js_buffer(bytes, env)?,\n        MontyObject::List(items) => create_js_array(items, env)?.into_unknown(env)?,\n        MontyObject::Tuple(items) => create_js_tuple(items, env)?,\n        // NamedTuple is converted to a tuple (loses named access in JS)\n        MontyObject::NamedTuple { values, .. } => create_js_tuple(values, env)?,\n        MontyObject::Dict(pairs) => create_js_map(pairs, env)?,\n        MontyObject::Set(items) | MontyObject::FrozenSet(items) => create_js_set(items, env)?,\n        MontyObject::Exception { exc_type, arg } => create_js_exception(*exc_type, arg.as_deref(), env)?,\n        MontyObject::Type(t) => create_js_type_marker(&t.to_string(), env)?,\n        MontyObject::BuiltinFunction(f) => create_js_builtin_function_marker(&f.to_string(), env)?,\n        MontyObject::Dataclass {\n            name,\n            type_id,\n            field_names,\n            attrs,\n            frozen,\n        } => create_js_dataclass(name, *type_id, field_names, attrs, *frozen, env)?,\n        MontyObject::Path(p) => env.create_string(p)?.into_unknown(env)?,\n        MontyObject::Repr(s) | MontyObject::Cycle(_, s) => env.create_string(s)?.into_unknown(env)?,\n        // Function objects are internal to the name lookup protocol and should not normally\n        // appear as final output values. If they do, represent as a string with the function name.\n        MontyObject::Function { name, .. } => env.create_string(name)?.into_unknown(env)?,\n    };\n    Ok(JsMontyObject(unknown))\n}\n\n/// Creates a JS null value.\nfn create_js_null(env: &Env) -> Result<Unknown<'_>> {\n    // Use raw napi to create null\n    let mut result = std::ptr::null_mut();\n    // SAFETY: [DH] - all arguments are valid and result is valid on success\n    unsafe {\n        let status = sys::napi_get_null(env.raw(), &raw mut result);\n        if status != sys::Status::napi_ok {\n            return Err(Error::from_reason(\"Failed to create null\"));\n        }\n        Ok(Unknown::from_raw_unchecked(env.raw(), result))\n    }\n}\n\n/// Creates a JS boolean value.\nfn create_js_bool(b: bool, env: &Env) -> Result<Unknown<'_>> {\n    let mut result = std::ptr::null_mut();\n    // SAFETY: [DH] - all arguments are valid and result is valid on success\n    unsafe {\n        let status = sys::napi_get_boolean(env.raw(), b, &raw mut result);\n        if status != sys::Status::napi_ok {\n            return Err(Error::from_reason(\"Failed to create boolean\"));\n        }\n        Ok(Unknown::from_raw_unchecked(env.raw(), result))\n    }\n}\n\n/// Creates a JS number or BigInt depending on whether the value fits in JS safe integer range.\nfn create_js_int(i: i64, env: &Env) -> Result<Unknown<'_>> {\n    if (JS_SAFE_INT_MIN..=JS_SAFE_INT_MAX).contains(&i) {\n        env.create_int64(i)?.into_unknown(env)\n    } else {\n        // Use BigInt for large integers\n        BigInt::from(i).into_unknown(env)\n    }\n}\n\n/// Creates a native JS BigInt from an arbitrary-precision integer.\n///\n/// For integers that fit in i64, uses direct creation. For larger integers,\n/// calls the global `BigInt()` constructor with the decimal string representation.\nfn create_js_bigint<'e>(bi: &NumBigInt, env: &'e Env) -> Result<Unknown<'e>> {\n    // Try to fit in i64 first for efficiency\n    if let Ok(i) = i64::try_from(bi) {\n        return BigInt::from(i).into_unknown(env);\n    }\n\n    // For larger integers, call global BigInt(string)\n    let global = env.get_global()?;\n    let bigint_constructor: Function<String> = global.get_named_property(\"BigInt\")?;\n    let result = bigint_constructor.call(bi.to_string())?;\n    result.into_unknown(env)\n}\n\n/// Creates a Node.js Buffer from bytes.\nfn create_js_buffer<'e>(bytes: &[u8], env: &'e Env) -> Result<Unknown<'e>> {\n    let buffer = BufferSlice::from_data(env, bytes.to_vec())?;\n    buffer.into_unknown(env)\n}\n\n/// Creates a native JS Array from Monty list items, recursively converting each element.\nfn create_js_array<'e>(items: &[MontyObject], env: &'e Env) -> Result<Array<'e>> {\n    let mut arr = env.create_array(items.len().try_into().expect(\"array size overflows u32\"))?;\n    for (i, item) in items.iter().enumerate() {\n        let js_item = monty_to_js(item, env)?;\n        arr.set(i.try_into().expect(\"overflow on array index\"), js_item)?;\n    }\n    Ok(arr)\n}\n\n/// Creates a tuple representation as a JS array with a `__tuple__` marker property.\n///\n/// This allows distinguishing tuples from lists in JavaScript while still allowing\n/// array-like access to tuple elements.\nfn create_js_tuple<'e>(items: &[MontyObject], env: &'e Env) -> Result<Unknown<'e>> {\n    let mut arr = create_js_array(items, env)?;\n    arr.set_named_property(\"__tuple__\", true)?;\n    arr.into_unknown(env)\n}\n\n/// Creates a native JS `Map` from Monty dict pairs, recursively converting keys and values.\n///\n/// Using `Map` instead of plain objects preserves:\n/// - Non-string key types (numbers, booleans, etc.)\n/// - Insertion order\n/// - Proper equality semantics for keys\nfn create_js_map<'e>(pairs: &DictPairs, env: &'e Env) -> Result<Unknown<'e>> {\n    let global = env.get_global()?;\n    let map_constructor: Function<()> = global.get_named_property(\"Map\")?;\n    let map: Object<'e> = map_constructor.new_instance(())?.coerce_to_object()?;\n\n    let set_method: Unknown = map.get_named_property(\"set\")?;\n    for (k, v) in pairs {\n        let js_key = monty_to_js(k, env)?;\n        let js_value = monty_to_js(v, env)?;\n        // Call map.set(key, value) using raw napi to pass two separate arguments\n        call_method_2_args(env.raw(), map.raw(), set_method.raw(), js_key.0.raw(), js_value.0.raw())?;\n    }\n    map.into_unknown(env)\n}\n\n/// Calls a JS method with 2 arguments using raw napi.\n///\n/// This is needed because napi-rs's `Function::apply` with tuple args doesn't work correctly\n/// for methods expecting two separate arguments.\nfn call_method_2_args(\n    env: sys::napi_env,\n    this: sys::napi_value,\n    method: sys::napi_value,\n    arg1: sys::napi_value,\n    arg2: sys::napi_value,\n) -> Result<()> {\n    let args = [arg1, arg2];\n    let mut result = std::ptr::null_mut();\n    // SAFETY: [DH] - all arguments are valid and result is valid on success\n    unsafe {\n        let status = sys::napi_call_function(env, this, method, 2, args.as_ptr(), &raw mut result);\n        if status != sys::Status::napi_ok {\n            return Err(Error::from_reason(\"Failed to call method\"));\n        }\n    }\n    Ok(())\n}\n\n/// Creates a native JS Set from Monty set items.\nfn create_js_set<'e>(items: &[MontyObject], env: &'e Env) -> Result<Unknown<'e>> {\n    let global = env.get_global()?;\n    let set_constructor: Function<()> = global.get_named_property(\"Set\")?;\n    let set: Object<'e> = set_constructor.new_instance(())?.coerce_to_object()?;\n\n    let add_method: Function = set.get_named_property(\"add\")?;\n    for item in items {\n        let js_item = monty_to_js(item, env)?;\n        add_method.apply(set, js_item.0)?;\n    }\n    set.into_unknown(env)\n}\n\n/// Creates a JS object representing Ellipsis: `{ __monty_type__: 'Ellipsis' }`.\nfn create_js_ellipsis(env: &Env) -> Result<Unknown<'_>> {\n    let mut obj = Object::new(env)?;\n    obj.set_named_property(\"__monty_type__\", \"Ellipsis\")?;\n    obj.into_unknown(env)\n}\n\n/// Creates a JS object representing an exception.\nfn create_js_exception<'e>(exc_type: ExcType, arg: Option<&str>, env: &'e Env) -> Result<Unknown<'e>> {\n    let mut obj = Object::new(env)?;\n    obj.set_named_property(\"__monty_type__\", \"Exception\")?;\n    obj.set_named_property(\"excType\", exc_type.to_string())?;\n    obj.set_named_property(\"message\", arg.unwrap_or(\"\"))?;\n    obj.into_unknown(env)\n}\n\n/// Creates a JS object representing a Type: `{ __monty_type__: 'Type', value: '...' }`.\nfn create_js_type_marker<'e>(type_str: &str, env: &'e Env) -> Result<Unknown<'e>> {\n    let mut obj = Object::new(env)?;\n    obj.set_named_property(\"__monty_type__\", \"Type\")?;\n    obj.set_named_property(\"value\", type_str)?;\n    obj.into_unknown(env)\n}\n\n/// Creates a JS object representing a builtin function.\nfn create_js_builtin_function_marker<'e>(func_str: &str, env: &'e Env) -> Result<Unknown<'e>> {\n    let mut obj = Object::new(env)?;\n    obj.set_named_property(\"__monty_type__\", \"BuiltinFunction\")?;\n    obj.set_named_property(\"value\", func_str)?;\n    obj.into_unknown(env)\n}\n\n/// Creates a JS object representing a dataclass instance.\nfn create_js_dataclass<'e>(\n    name: &str,\n    type_id: u64,\n    field_names: &[String],\n    attrs: &DictPairs,\n    frozen: bool,\n    env: &'e Env,\n) -> Result<Unknown<'e>> {\n    let mut obj = Object::new(env)?;\n    obj.set_named_property(\"__monty_type__\", \"Dataclass\")?;\n    obj.set_named_property(\"name\", name)?;\n\n    // type_id as BigInt since it may exceed JS safe integer range\n    let type_id_bigint = BigInt::from(type_id);\n    obj.set_named_property(\"typeId\", type_id_bigint)?;\n\n    // field_names as array\n    let mut field_names_arr =\n        env.create_array(field_names.len().try_into().expect(\"field_names size overflows u32\"))?;\n    for (i, field_name) in field_names.iter().enumerate() {\n        field_names_arr.set(\n            i.try_into().expect(\"overflow on field_names index\"),\n            env.create_string(field_name)?,\n        )?;\n    }\n    obj.set_named_property(\"fieldNames\", field_names_arr)?;\n\n    // Build attrs as a nested object mapping field names to values\n    let attrs_map: HashMap<&str, &MontyObject> = attrs\n        .into_iter()\n        .filter_map(|(k, v)| {\n            if let MontyObject::String(key) = k {\n                Some((key.as_str(), v))\n            } else {\n                None\n            }\n        })\n        .collect();\n\n    let mut fields_obj = Object::new(env)?;\n    for field_name in field_names {\n        if let Some(value) = attrs_map.get(field_name.as_str()) {\n            let js_value = monty_to_js(value, env)?;\n            fields_obj.set_named_property(field_name.as_str(), js_value)?;\n        }\n    }\n    obj.set_named_property(\"fields\", fields_obj)?;\n\n    obj.set_named_property(\"frozen\", frozen)?;\n\n    obj.into_unknown(env)\n}\n\n// =============================================================================\n// JS to Monty conversion\n// =============================================================================\n\n/// Converts a JavaScript value to Monty's `MontyObject`.\n///\n/// This function handles native JS types and marked objects:\n/// - `null` → `None`\n/// - `boolean` → `Bool`\n/// - `number` → `Int` (if integer) or `Float`\n/// - `bigint` → `Int` (if fits in i64) or `BigInt`\n/// - `string` → `String`\n/// - `Buffer`/`Uint8Array` → `Bytes`\n/// - `Array` with `__tuple__` → `Tuple`\n/// - `Array` → `List`\n/// - `Map` → `Dict`\n/// - `Set` → `Set`\n/// - `Object` with `__monty_type__` → corresponding Monty type\n/// - `Object` → `Dict` (string keys only)\npub fn js_to_monty(value: Unknown<'_>, env: Env) -> Result<MontyObject> {\n    let value_type = value.get_type()?;\n\n    match value_type {\n        ValueType::Null | ValueType::Undefined => Ok(MontyObject::None),\n        ValueType::Boolean => {\n            let b: bool = value.coerce_to_bool()?;\n            Ok(MontyObject::Bool(b))\n        }\n        ValueType::Number => {\n            let n: f64 = value.coerce_to_number()?.get_double()?;\n            // Check if the number is actually an integer (no fractional part)\n            // and fits within i64 range\n            if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {\n                #[expect(\n                    clippy::cast_possible_truncation,\n                    reason = \"Checked above that n is integer and within i64 range\"\n                )]\n                return Ok(MontyObject::Int(n as i64));\n            }\n            Ok(MontyObject::Float(n))\n        }\n        ValueType::BigInt => {\n            let bigint: BigInt = BigInt::from_unknown(value)?;\n\n            // BigInt has public fields: sign_bit (bool) and words (Vec<u64>)\n            // Convert words (u64 array) to num-bigint::BigInt\n            // Each word is a 64-bit limb, little-endian order\n            if bigint.words.is_empty() {\n                return Ok(MontyObject::Int(0));\n            }\n\n            let mut bi = NumBigInt::from(0u64);\n            for (i, &word) in bigint.words.iter().enumerate() {\n                let limb = NumBigInt::from(word);\n                bi += limb << (64 * i);\n            }\n\n            if bigint.sign_bit {\n                bi = -bi;\n            }\n\n            // Try to fit in i64\n            if let Ok(i) = i64::try_from(&bi) {\n                Ok(MontyObject::Int(i))\n            } else {\n                Ok(MontyObject::BigInt(bi))\n            }\n        }\n        ValueType::String => {\n            let s: String = value.coerce_to_string()?.into_utf8()?.into_owned()?;\n            Ok(MontyObject::String(s))\n        }\n        ValueType::Object => {\n            let obj: Object = value.coerce_to_object()?;\n\n            // Check if it's a Buffer (Uint8Array)\n            if obj.is_buffer()? {\n                let buffer: BufferSlice = BufferSlice::from_unknown(value)?;\n                return Ok(MontyObject::Bytes(buffer.to_vec()));\n            }\n\n            // Check if it's a Map\n            if is_js_map(&obj, env)? {\n                return js_map_to_monty(obj, env);\n            }\n\n            // Check if it's a Set\n            if is_js_set(&obj, env)? {\n                return js_set_to_monty(obj, env);\n            }\n\n            // Check if it's an Array\n            if obj.is_array()? {\n                return js_array_to_monty(obj, env);\n            }\n\n            // Check for __monty_type__ marker\n            if let Some(monty_type) = get_string_property(&obj, \"__monty_type__\")? {\n                return js_marked_object_to_monty(&obj, &monty_type, env);\n            }\n\n            // Plain object → Dict (with string keys)\n            js_object_to_monty_dict(obj, env)\n        }\n        ValueType::Function => {\n            // JS functions are converted to MontyObject::Function for external function resolution.\n            // The function's `name` property is used as the Monty function name.\n            let func_obj: Object = value.coerce_to_object()?;\n            let name: String = func_obj\n                .get_named_property::<String>(\"name\")\n                .unwrap_or_else(|_| \"<anonymous>\".to_string());\n            Ok(MontyObject::Function { name, docstring: None })\n        }\n        ValueType::Symbol | ValueType::External => {\n            // These JS types don't have Monty equivalents\n            Err(Error::from_reason(format!(\n                \"Cannot convert JS {value_type:?} to Monty value\"\n            )))\n        }\n        // Unknown is not a real JS type, it's a napi-rs placeholder\n        ValueType::Unknown => Err(Error::from_reason(\"Unknown JS value type\")),\n    }\n}\n\n/// Checks if a JS object is an instance of Set.\nfn is_js_set(obj: &Object, env: Env) -> Result<bool> {\n    let global = env.get_global()?;\n    let set_constructor: Function<()> = global.get_named_property(\"Set\")?;\n    obj.instanceof(set_constructor)\n}\n\n/// Checks if a JS object is an instance of Map.\nfn is_js_map(obj: &Object, env: Env) -> Result<bool> {\n    let global = env.get_global()?;\n    let map_constructor: Function<()> = global.get_named_property(\"Map\")?;\n    obj.instanceof(map_constructor)\n}\n\n/// Converts a JS Map to `MontyObject::Dict`.\nfn js_map_to_monty(map: Object, env: Env) -> Result<MontyObject> {\n    // Get the entries iterator\n    let entries_method: Function<()> = map.get_named_property(\"entries\")?;\n    let iterator: Object = entries_method.apply(map, ())?.coerce_to_object()?;\n\n    let mut pairs = Vec::new();\n    loop {\n        let next_method: Function<()> = iterator.get_named_property(\"next\")?;\n        let result: Object = next_method.apply(iterator, ())?.coerce_to_object()?;\n\n        let done: bool = result.get_named_property::<bool>(\"done\")?;\n        if done {\n            break;\n        }\n\n        // value is [key, value] array\n        let entry: Object = result.get_named_property::<Unknown>(\"value\")?.coerce_to_object()?;\n        let key: Unknown = entry.get_element(0)?;\n        let value: Unknown = entry.get_element(1)?;\n\n        let monty_key = js_to_monty(key, env)?;\n        let monty_value = js_to_monty(value, env)?;\n        pairs.push((monty_key, monty_value));\n    }\n\n    Ok(MontyObject::dict(pairs))\n}\n\n/// Converts a JS Set to `MontyObject::Set`.\nfn js_set_to_monty(set: Object, env: Env) -> Result<MontyObject> {\n    // Get the values iterator\n    let values_method: Function<()> = set.get_named_property(\"values\")?;\n    let iterator: Object = values_method.apply(set, ())?.coerce_to_object()?;\n\n    let mut items = Vec::new();\n    loop {\n        let next_method: Function<()> = iterator.get_named_property(\"next\")?;\n        let result: Object = next_method.apply(iterator, ())?.coerce_to_object()?;\n\n        let done: bool = result.get_named_property::<bool>(\"done\")?;\n        if done {\n            break;\n        }\n\n        let value: Unknown = result.get_named_property(\"value\")?;\n        items.push(js_to_monty(value, env)?);\n    }\n\n    Ok(MontyObject::Set(items))\n}\n\n/// Converts a JS Array to `MontyObject::List` or `MontyObject::Tuple`.\nfn js_array_to_monty(arr: Object, env: Env) -> Result<MontyObject> {\n    let is_tuple: bool = arr.get_named_property::<Option<bool>>(\"__tuple__\")?.unwrap_or(false);\n\n    let length: u32 = arr.get_named_property(\"length\")?;\n    let mut items = Vec::with_capacity(length as usize);\n\n    for i in 0..length {\n        let element: Unknown = arr.get_element(i)?;\n        items.push(js_to_monty(element, env)?);\n    }\n\n    if is_tuple {\n        Ok(MontyObject::Tuple(items))\n    } else {\n        Ok(MontyObject::List(items))\n    }\n}\n\n/// Converts a JS object with `__monty_type__` marker to the appropriate `MontyObject`.\nfn js_marked_object_to_monty(obj: &Object, monty_type: &str, env: Env) -> Result<MontyObject> {\n    match monty_type {\n        \"Ellipsis\" => Ok(MontyObject::Ellipsis),\n        \"Exception\" => {\n            let exc_type_str: String = obj.get_named_property(\"excType\")?;\n            let message: String = obj.get_named_property(\"message\")?;\n            let exc_type: ExcType = exc_type_str\n                .parse()\n                .map_err(|_| Error::from_reason(format!(\"Unknown exception type: {exc_type_str}\")))?;\n            let arg = if message.is_empty() { None } else { Some(message) };\n            Ok(MontyObject::Exception { exc_type, arg })\n        }\n        \"Type\" => {\n            // Type objects can't be fully round-tripped; return as Repr\n            let value: String = obj.get_named_property(\"value\")?;\n            Ok(MontyObject::Repr(format!(\"<class '{value}'>\")))\n        }\n        \"BuiltinFunction\" => {\n            // BuiltinFunction objects can't be fully round-tripped; return as Repr\n            let value: String = obj.get_named_property(\"value\")?;\n            Ok(MontyObject::Repr(format!(\"<built-in function {value}>\")))\n        }\n        \"Dataclass\" => {\n            let name: String = obj.get_named_property(\"name\")?;\n\n            // type_id is BigInt - access its public fields\n            let type_id_bigint: BigInt = obj.get_named_property(\"typeId\")?;\n            let type_id = if type_id_bigint.words.is_empty() {\n                0u64\n            } else if type_id_bigint.sign_bit {\n                return Err(Error::from_reason(\"Dataclass typeId cannot be negative\"));\n            } else {\n                type_id_bigint.words[0]\n            };\n\n            // field_names\n            let field_names_arr: Array = obj.get_named_property(\"fieldNames\")?;\n            let field_names_len = field_names_arr.len();\n            let mut field_names = Vec::with_capacity(field_names_len as usize);\n            for i in 0..field_names_len {\n                let name: String = field_names_arr.get::<String>(i)?.unwrap_or_default();\n                field_names.push(name);\n            }\n\n            // fields object\n            let fields_obj: Object = obj.get_named_property(\"fields\")?;\n            let mut attrs_vec = Vec::new();\n            for field_name in &field_names {\n                if let Some(value) = fields_obj.get_named_property::<Option<Unknown>>(field_name.as_str())? {\n                    let monty_value = js_to_monty(value, env)?;\n                    attrs_vec.push((MontyObject::String(field_name.clone()), monty_value));\n                }\n            }\n            let attrs = DictPairs::from(attrs_vec);\n\n            let frozen: bool = obj.get_named_property(\"frozen\")?;\n\n            Ok(MontyObject::Dataclass {\n                name,\n                type_id,\n                field_names,\n                attrs,\n                frozen,\n            })\n        }\n        _ => {\n            // Unknown marker type, treat as dict\n            js_object_to_monty_dict(*obj, env)\n        }\n    }\n}\n\n/// Converts a plain JS object to `MontyObject::Dict`.\n///\n/// This is a fallback for plain objects (not Map instances). Since JS object keys\n/// are always strings, all keys in the resulting Dict will be strings.\n/// For full key type preservation, use JS `Map` instead.\nfn js_object_to_monty_dict(obj: Object, env: Env) -> Result<MontyObject> {\n    let keys = obj.get_property_names()?;\n    // Get length by accessing the \"length\" property\n    let length: u32 = keys.get_named_property(\"length\")?;\n    let mut pairs = Vec::with_capacity(length as usize);\n\n    for i in 0..length {\n        let key: Unknown = keys.get_element(i)?;\n        let key_str: String = key.coerce_to_string()?.into_utf8()?.into_owned()?;\n        let value: Unknown = obj.get_named_property(&key_str)?;\n        let monty_value = js_to_monty(value, env)?;\n        pairs.push((MontyObject::String(key_str), monty_value));\n    }\n\n    Ok(MontyObject::dict(pairs))\n}\n\n/// Helper to get an optional string property from a JS object.\nfn get_string_property(obj: &Object, name: &str) -> Result<Option<String>> {\n    let has_property = obj.has_named_property(name)?;\n    if !has_property {\n        return Ok(None);\n    }\n\n    let value: Unknown = obj.get_named_property(name)?;\n    if value.get_type()? == ValueType::String {\n        let s: String = value.coerce_to_string()?.into_utf8()?.into_owned()?;\n        Ok(Some(s))\n    } else {\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "crates/monty-js/src/exceptions.rs",
    "content": "//! Exception types for the Monty TypeScript/JavaScript bindings.\n//!\n//! This module provides thin napi wrappers around Monty's internal exceptions.\n//! The JavaScript wrapper layer (`wrapper.js`) is responsible for converting\n//! these into proper JS `Error` subclasses (`MontySyntaxError`, `MontyRuntimeError`).\n//!\n//! It is done this way because `napi` has no way to create JS `Error` subclasses from\n//! Rust.\n//!\n//! ## Architecture\n//!\n//! - `JsMontyException`: Thin wrapper around `monty::MontyException`. The JS wrapper\n//!   checks `exception.typeName` to distinguish syntax errors from runtime errors.\n//! - `MontyTypingError`: Wraps `TypeCheckingDiagnostics` for static type checking errors.\n//!   This is separate because type errors come from static analysis, not Python execution.\n\nuse std::fmt;\n\nuse monty::StackFrame;\nuse monty_type_checking::TypeCheckingDiagnostics;\nuse napi::bindgen_prelude::*;\nuse napi_derive::napi;\nuse serde::{Deserialize, Serialize};\n\n// =============================================================================\n// JsMontyException - Thin wrapper around core MontyException\n// =============================================================================\n\n/// Wrapper around core `MontyException` for napi bindings.\n///\n/// This is a thin newtype wrapper that exposes the necessary getters for the\n/// JavaScript wrapper to construct appropriate error types (`MontySyntaxError`\n/// or `MontyRuntimeError`) based on the exception type.\n#[napi(js_name = \"MontyException\")]\npub struct JsMontyException(monty::MontyException);\n\nimpl fmt::Display for JsMontyException {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\n#[napi]\nimpl JsMontyException {\n    /// Returns information about the inner Python exception.\n    ///\n    /// The `typeName` field can be used to distinguish syntax errors (`\"SyntaxError\"`)\n    /// from runtime errors (e.g., `\"ValueError\"`, `\"TypeError\"`).\n    #[napi(getter)]\n    #[must_use]\n    pub fn exception(&self) -> ExceptionInfo {\n        ExceptionInfo {\n            type_name: self.0.exc_type().to_string(),\n            message: self.0.message().unwrap_or_default().to_string(),\n        }\n    }\n\n    /// Returns the error message.\n    #[napi(getter)]\n    #[must_use]\n    pub fn message(&self) -> String {\n        self.0.message().unwrap_or_default().to_string()\n    }\n\n    /// Returns the Monty traceback as an array of Frame objects.\n    ///\n    /// For syntax errors, this will be an empty array.\n    /// For runtime errors, this contains the stack frames where the error occurred.\n    #[napi]\n    pub fn traceback(&self) -> Vec<Frame> {\n        self.0.traceback().iter().map(Frame::from_stack_frame).collect()\n    }\n\n    /// Returns formatted exception string.\n    ///\n    /// @param format - Output format:\n    ///   - 'traceback' - Full traceback (default)\n    ///   - 'type-msg' - 'ExceptionType: message' format\n    ///   - 'msg' - just the message\n    #[napi]\n    pub fn display(&self, format: Option<String>) -> Result<String> {\n        let format = format.as_deref().unwrap_or(\"traceback\");\n        match format {\n            \"traceback\" => Ok(self.0.to_string()),\n            \"type-msg\" => {\n                let type_name = self.0.exc_type().to_string();\n                let message = self.0.message().unwrap_or_default();\n                if message.is_empty() {\n                    Ok(type_name)\n                } else {\n                    Ok(format!(\"{type_name}: {message}\"))\n                }\n            }\n            \"msg\" => Ok(self.0.message().unwrap_or_default().to_string()),\n            _ => Err(Error::from_reason(format!(\n                \"Invalid display format: '{format}'. Expected 'traceback', 'type-msg', or 'msg'\"\n            ))),\n        }\n    }\n\n    /// Returns a string representation of the error.\n    #[napi(js_name = \"toString\")]\n    #[must_use]\n    pub fn to_js_string(&self) -> String {\n        self.to_string()\n    }\n}\n\nimpl JsMontyException {\n    /// Creates a new JsMontyException from a core MontyException.\n    #[must_use]\n    pub fn new(exc: monty::MontyException) -> Self {\n        Self(exc)\n    }\n}\n\n// =============================================================================\n// MontyTypingError - Raised when type checking finds errors\n// =============================================================================\n\n/// Raised when type checking finds errors in the code.\n///\n/// This exception is raised when static type analysis detects type errors.\n/// Use `display()` to render diagnostics in various formats.\n#[napi]\npub struct MontyTypingError {\n    /// The type checking failure containing diagnostic information.\n    failure: TypeCheckingDiagnostics,\n    /// Cached string representation.\n    cached_string: String,\n}\n\nimpl fmt::Display for MontyTypingError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.cached_string)\n    }\n}\n\n#[napi]\nimpl MontyTypingError {\n    /// Returns information about the inner exception.\n    #[napi(getter)]\n    #[must_use]\n    pub fn exception(&self) -> ExceptionInfo {\n        ExceptionInfo {\n            type_name: \"TypeError\".to_string(),\n            message: self.cached_string.clone(),\n        }\n    }\n\n    /// Returns the error message.\n    #[napi(getter)]\n    #[must_use]\n    pub fn message(&self) -> String {\n        self.cached_string.clone()\n    }\n\n    /// Renders the type error diagnostics with the specified format and color.\n    ///\n    /// @param format - Output format. One of:\n    ///   - 'full' - Full diagnostic output (default)\n    ///   - 'concise' - Concise output\n    ///   - 'azure' - Azure DevOps format\n    ///   - 'json' - JSON format\n    ///   - 'jsonlines' - JSON Lines format\n    ///   - 'rdjson' - RDJson format\n    ///   - 'pylint' - Pylint format\n    ///   - 'gitlab' - GitLab CI format\n    ///   - 'github' - GitHub Actions format\n    /// @param color - Whether to include ANSI color codes. Default: false\n    #[napi]\n    pub fn display(&self, format: Option<String>, color: Option<bool>) -> Result<String> {\n        let format = format.as_deref().unwrap_or(\"full\");\n        let color = color.unwrap_or(false);\n\n        self.failure\n            .clone()\n            .color(color)\n            .format_from_str(format)\n            .map_err(Error::from_reason)\n            .map(|f| f.to_string())\n    }\n\n    /// Returns a string representation of the error.\n    #[napi(js_name = \"toString\")]\n    #[must_use]\n    pub fn to_js_string(&self) -> String {\n        self.to_string()\n    }\n}\n\nimpl MontyTypingError {\n    /// Creates a MontyTypingError from a TypeCheckingDiagnostics.\n    #[must_use]\n    pub fn from_failure(failure: TypeCheckingDiagnostics) -> Self {\n        let cached_string = failure.to_string();\n        Self { failure, cached_string }\n    }\n}\n\n// =============================================================================\n// Helper types\n// =============================================================================\n\n/// Information about the inner Python exception.\n///\n/// This provides structured access to the exception type and message\n/// for programmatic error handling.\n#[napi(object)]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ExceptionInfo {\n    /// The exception type name (e.g., \"ValueError\", \"TypeError\", \"SyntaxError\").\n    pub type_name: String,\n    /// The exception message.\n    pub message: String,\n}\n\n/// A single frame in a Monty traceback.\n///\n/// Contains all the information needed to display a traceback line:\n/// the file location, function name, and optional source code preview.\n#[napi(object)]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Frame {\n    /// The filename where the code is located.\n    pub filename: String,\n    /// Line number (1-based).\n    pub line: u32,\n    /// Column number (1-based).\n    pub column: u32,\n    /// End line number (1-based).\n    pub end_line: u32,\n    /// End column number (1-based).\n    pub end_column: u32,\n    /// The name of the function, or null for module-level code.\n    pub function_name: Option<String>,\n    /// The source code line for preview in the traceback.\n    pub source_line: Option<String>,\n}\n\nimpl Frame {\n    /// Creates a `Frame` from Monty's `StackFrame`.\n    #[must_use]\n    pub fn from_stack_frame(frame: &StackFrame) -> Self {\n        Self {\n            filename: frame.filename.clone(),\n            line: u32::from(frame.start.line),\n            column: u32::from(frame.start.column),\n            end_line: u32::from(frame.end.line),\n            end_column: u32::from(frame.end.column),\n            function_name: frame.frame_name.clone(),\n            source_line: frame.preview_line.clone(),\n        }\n    }\n}\n\n/// Converts a javascript error into a MontyException.\npub fn exc_js_to_monty(js_err: napi::Error) -> ::monty::MontyException {\n    let exc = js_err_to_exc_type(js_err.status);\n    let arg = js_err.reason.clone();\n\n    ::monty::MontyException::new(exc, Some(arg))\n}\n\nfn js_err_to_exc_type(exc: napi::Status) -> ::monty::ExcType {\n    use ::monty::ExcType;\n    match exc {\n        napi::Status::Ok => ExcType::Exception, // Should never happen\n        napi::Status::InvalidArg => ExcType::TypeError,\n        napi::Status::ObjectExpected\n        | napi::Status::StringExpected\n        | napi::Status::NameExpected\n        | napi::Status::FunctionExpected\n        | napi::Status::NumberExpected\n        | napi::Status::BooleanExpected\n        | napi::Status::ArrayExpected\n        | napi::Status::BigintExpected\n        | napi::Status::DateExpected\n        | napi::Status::ArrayBufferExpected\n        | napi::Status::DetachableArraybufferExpected\n        | napi::Status::HandleScopeMismatch\n        | napi::Status::CallbackScopeMismatch => ExcType::ValueError,\n        napi::Status::GenericFailure => ExcType::Exception,\n        napi::Status::Cancelled => ExcType::KeyboardInterrupt,\n        napi::Status::QueueFull\n        | napi::Status::Closing\n        | napi::Status::WouldDeadlock\n        | napi::Status::NoExternalBuffersAllowed\n        | napi::Status::PendingException\n        | napi::Status::EscapeCalledTwice => ExcType::RuntimeError,\n        napi::Status::Unknown => ExcType::Exception,\n    }\n}\n"
  },
  {
    "path": "crates/monty-js/src/lib.rs",
    "content": "// napi macros generate code that triggers some clippy lints\n#![allow(clippy::needless_pass_by_value, clippy::trivially_copy_pass_by_ref)]\n\n//! Node.js/TypeScript bindings for the Monty sandboxed Python interpreter.\n//!\n//! This module provides a JavaScript/TypeScript interface to Monty via napi-rs,\n//! allowing execution of sandboxed Python code from Node.js with configurable\n//! inputs, resource limits, and external function callbacks.\n//!\n//! ## Quick Start\n//!\n//! ```typescript\n//! import { Monty } from 'monty';\n//!\n//! // Simple execution\n//! const m = new Monty('1 + 2');\n//! const result = m.run(); // returns 3\n//!\n//! // With inputs\n//! const m2 = new Monty('x + y', { inputs: ['x', 'y'] });\n//! const result2 = m2.run({ inputs: { x: 10, y: 20 } }); // returns 30\n//!\n//! // Iterative execution with external functions\n//! const m3 = new Monty('external_func()');\n//! let progress = m3.start();\n//! if (progress instanceof MontySnapshot) {\n//!     progress = progress.resume({ returnValue: 42 });\n//! }\n//! ```\n\nmod convert;\nmod exceptions;\nmod limits;\nmod monty_cls;\n\npub use exceptions::{ExceptionInfo, Frame, JsMontyException, MontyTypingError};\npub use limits::JsResourceLimits;\npub use monty_cls::{\n    ExceptionInput, Monty, MontyComplete, MontyNameLookup, MontyOptions, MontyRepl, MontySnapshot,\n    NameLookupLoadOptions, NameLookupResumeOptions, ResumeOptions, RunOptions, SnapshotLoadOptions, StartOptions,\n};\n"
  },
  {
    "path": "crates/monty-js/src/limits.rs",
    "content": "//! Resource limits handling for the Monty TypeScript/JavaScript bindings.\n//!\n//! Provides utilities to extract and apply resource limits from JavaScript objects,\n//! including time limits, memory limits, and recursion depth.\n\nuse std::time::Duration;\n\nuse monty::{ResourceLimits, DEFAULT_MAX_RECURSION_DEPTH};\nuse napi_derive::napi;\n\n/// Resource limits configuration from JavaScript.\n///\n/// All limits are optional. Omit a key to disable that limit.\n#[napi(object, js_name = \"ResourceLimits\")]\n#[derive(Debug, Clone, Copy, Default)]\npub struct JsResourceLimits {\n    /// Maximum number of heap allocations allowed.\n    pub max_allocations: Option<u32>,\n    /// Maximum execution time in seconds.\n    pub max_duration_secs: Option<f64>,\n    /// Maximum heap memory in bytes.\n    pub max_memory: Option<u32>,\n    /// Run garbage collection every N allocations.\n    pub gc_interval: Option<u32>,\n    /// Maximum function call stack depth (default: 1000).\n    pub max_recursion_depth: Option<u32>,\n}\n\nimpl From<JsResourceLimits> for ResourceLimits {\n    fn from(js_limits: JsResourceLimits) -> Self {\n        let max_recursion_depth = js_limits\n            .max_recursion_depth\n            .map(|v| v as usize)\n            .or(Some(DEFAULT_MAX_RECURSION_DEPTH));\n\n        let mut limits = Self::new().max_recursion_depth(max_recursion_depth);\n\n        if let Some(max) = js_limits.max_allocations {\n            limits = limits.max_allocations(max as usize);\n        }\n        if let Some(secs) = js_limits.max_duration_secs {\n            limits = limits.max_duration(Duration::from_secs_f64(secs));\n        }\n        if let Some(max) = js_limits.max_memory {\n            limits = limits.max_memory(max as usize);\n        }\n        if let Some(interval) = js_limits.gc_interval {\n            limits = limits.gc_interval(interval as usize);\n        }\n\n        limits\n    }\n}\n"
  },
  {
    "path": "crates/monty-js/src/monty_cls.rs",
    "content": "//! The main `Monty` class and iterative execution support for the TypeScript/JavaScript bindings.\n//!\n//! Provides a sandboxed Python interpreter that can be configured with inputs\n//! and resource limits. External functions are provided at runtime via\n//! `RunOptions` or `StartOptions`. Supports both immediate execution\n//! via `run()` and iterative execution via `start()`/`resume()`.\n//!\n//! ## Quick Start\n//!\n//! ```typescript\n//! import { Monty } from 'monty';\n//!\n//! // Simple execution\n//! const m = new Monty('1 + 2');\n//! const result = m.run(); // returns 3\n//!\n//! // With inputs\n//! const m2 = new Monty('x + y', { inputs: ['x', 'y'] });\n//! const result2 = m2.run({ inputs: { x: 10, y: 20 } }); // returns 30\n//! ```\n//!\n//! ## Iterative Execution\n//!\n//! ```text\n//! Monty.start() -> MontySnapshot | MontyNameLookup | MontyComplete\n//!                       |                |\n//!                       v                v\n//! MontySnapshot.resume() / MontyNameLookup.resume()\n//!       -> MontySnapshot | MontyNameLookup | MontyComplete\n//!                       |                |\n//!                       v                v\n//!                    (repeat until complete)\n//! ```\n//!\n//! ```typescript\n//! const m = new Monty('result = external_func(1, 2)');\n//!\n//! let progress = m.start();\n//! while (progress instanceof MontySnapshot) {\n//!   console.log(`Calling ${progress.functionName} with args:`, progress.args);\n//!   progress = progress.resume({ returnValue: 42 });\n//! }\n//! console.log('Final result:', progress.output);\n//! ```\n\nuse std::borrow::Cow;\n\nuse monty::{\n    ExcType, ExtFunctionResult, FunctionCall, LimitedTracker, MontyException, MontyObject, MontyRepl as CoreMontyRepl,\n    MontyRun, NameLookup, NameLookupResult, NoLimitTracker, OsCall, PrintWriter, PrintWriterCallback, ResourceTracker,\n    RunProgress,\n};\nuse monty_type_checking::{type_check, SourceFile};\nuse napi::bindgen_prelude::*;\nuse napi_derive::napi;\n\nuse crate::{\n    convert::{js_to_monty, monty_to_js, JsMontyObject},\n    exceptions::{exc_js_to_monty, JsMontyException, MontyTypingError},\n    limits::JsResourceLimits,\n};\n\n// =============================================================================\n// Monty - Main interpreter class\n// =============================================================================\n\n/// A sandboxed Python interpreter instance.\n///\n/// Parses and compiles Python code on initialization, then can be run\n/// multiple times with different input values. This separates the parsing\n/// cost from execution, making repeated runs more efficient.\n#[napi]\npub struct Monty {\n    /// The compiled code runner, ready to execute.\n    runner: MontyRun,\n    /// The artificial name of the python code \"file\".\n    script_name: String,\n    /// Names of input variables expected by the code.\n    input_names: Vec<String>,\n}\n\n/// Options for creating a new Monty instance.\n#[napi(object)]\n#[derive(Default)]\npub struct MontyOptions {\n    /// Name used in tracebacks and error messages. Default: 'main.py'\n    pub script_name: Option<String>,\n    /// List of input variable names available in the code.\n    pub inputs: Option<Vec<String>>,\n    /// Whether to perform type checking on the code. Default: false\n    pub type_check: Option<bool>,\n    /// Optional code to prepend before type checking.\n    pub type_check_prefix_code: Option<String>,\n}\n\n/// Options for running code.\n#[napi(object)]\n#[derive(Default)]\npub struct RunOptions<'env> {\n    pub inputs: Option<Object<'env>>,\n    /// Resource limits configuration.\n    pub limits: Option<JsResourceLimits>,\n    /// Optional print callback function.\n    pub print_callback: Option<JsPrintCallback<'env>>,\n    /// Dict of external function callbacks.\n    /// Keys are function names, values are callable functions.\n    pub external_functions: Option<Object<'env>>,\n}\n\n/// Options for starting execution.\n#[napi(object)]\n#[derive(Default)]\npub struct StartOptions<'env> {\n    /// Dict of input variable values.\n    pub inputs: Option<Object<'env>>,\n    /// Resource limits configuration.\n    pub limits: Option<JsResourceLimits>,\n    /// Optional print callback function.\n    pub print_callback: Option<JsPrintCallback<'env>>,\n}\n\n#[napi]\nimpl Monty {\n    /// Creates a new Monty interpreter by parsing the given code.\n    ///\n    /// Returns either a Monty instance, a MontyException (for syntax errors), or a MontyTypingError.\n    /// The wrapper should check the result type and throw the appropriate error.\n    ///\n    /// @param code - Python code to execute\n    /// @param options - Configuration options\n    /// @returns Monty instance on success, or error object on failure\n    #[napi]\n    pub fn create(\n        code: String,\n        options: Option<MontyOptions>,\n    ) -> Result<Either3<Self, JsMontyException, MontyTypingError>> {\n        let ResolvedMontyOptions {\n            script_name,\n            input_names,\n            do_type_check,\n            type_check_prefix_code,\n        } = resolve_monty_options(options);\n\n        // Perform type checking if requested\n        if do_type_check {\n            if let Some(error) = run_type_check_result(&code, &script_name, type_check_prefix_code.as_deref())? {\n                return Ok(Either3::C(error));\n            }\n        }\n\n        // Create the runner (parses the code)\n        let runner = match MontyRun::new(code, &script_name, input_names.clone()) {\n            Ok(r) => r,\n            Err(exc) => return Ok(Either3::B(JsMontyException::new(exc))),\n        };\n\n        Ok(Either3::A(Self {\n            runner,\n            script_name,\n            input_names,\n        }))\n    }\n\n    /// Performs static type checking on the code.\n    ///\n    /// Returns either nothing (success) or a MontyTypingError.\n    ///\n    /// @param prefixCode - Optional code to prepend before type checking\n    /// @returns null on success, or MontyTypingError on failure\n    #[napi]\n    pub fn type_check(&self, prefix_code: Option<String>) -> Result<Option<MontyTypingError>> {\n        run_type_check_result(self.runner.code(), &self.script_name, prefix_code.as_deref())\n    }\n\n    /// Executes the code and returns the result, or an exception object if execution fails.\n    ///\n    /// If runtime `externalFunctions` are provided, the start/resume loop is used\n    /// to dispatch external function calls and name lookups. Otherwise, code is\n    /// executed directly.\n    ///\n    /// @param options - Execution options (inputs, limits, externalFunctions)\n    /// @returns The result of the last expression, or a MontyException if execution fails\n    #[napi]\n    pub fn run<'env>(\n        &self,\n        env: &'env Env,\n        options: Option<RunOptions<'env>>,\n    ) -> Result<Either<JsMontyObject<'env>, JsMontyException>> {\n        let options = options.unwrap_or_default();\n        let input_values = self.extract_input_values(options.inputs, *env)?;\n\n        let external_functions = options.external_functions;\n\n        let mut print_cb;\n        let print_writer = match &options.print_callback {\n            Some(func) => {\n                print_cb = CallbackStringPrint::new_js(env, func)?;\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n\n        // If we have runtime external functions, use the start/resume loop\n        // to handle both FunctionCall and NameLookup dispatching\n        if external_functions.is_some() {\n            return self.run_with_external_functions(\n                env,\n                input_values,\n                options.limits,\n                external_functions,\n                print_writer,\n            );\n        }\n\n        let result = if let Some(limits) = options.limits {\n            let tracker = LimitedTracker::new(limits.into());\n            self.runner.run(input_values, tracker, print_writer)\n        } else {\n            let tracker = NoLimitTracker;\n            self.runner.run(input_values, tracker, print_writer)\n        };\n\n        match result {\n            Ok(value) => Ok(Either::A(monty_to_js(&value, env)?)),\n            Err(exc) => Ok(Either::B(JsMontyException::new(exc))),\n        }\n    }\n\n    /// Internal helper to run code with external function callbacks.\n    ///\n    /// Handles both `FunctionCall` and `NameLookup` dispatch in a loop.\n    /// For `NameLookup`, checks the runtime external functions map: if the name\n    /// is found, resolves it as a `Function`; otherwise returns `Undefined`.\n    fn run_with_external_functions<'env>(\n        &self,\n        env: &'env Env,\n        input_values: Vec<MontyObject>,\n        limits: Option<JsResourceLimits>,\n        external_functions: Option<Object<'env>>,\n        mut print_output: PrintWriter<'_>,\n    ) -> Result<Either<JsMontyObject<'env>, JsMontyException>> {\n        let runner = self.runner.clone();\n\n        // Helper macro to handle the execution loop for both tracker types\n        macro_rules! run_loop {\n            ($tracker:expr) => {{\n                let progress = runner.start(input_values, $tracker, print_output.reborrow());\n\n                let mut progress = match progress {\n                    Ok(p) => p,\n                    Err(exc) => return Ok(Either::B(JsMontyException::new(exc))),\n                };\n\n                loop {\n                    match progress {\n                        RunProgress::Complete(result) => {\n                            return Ok(Either::A(monty_to_js(&result, env)?));\n                        }\n                        RunProgress::FunctionCall(call) => {\n                            let return_value = call_external_function(\n                                env,\n                                external_functions.as_ref(),\n                                &call.function_name,\n                                &call.args,\n                                &call.kwargs,\n                            )?;\n\n                            progress = match call.resume(return_value, print_output.reborrow()) {\n                                Ok(p) => p,\n                                Err(exc) => return Ok(Either::B(JsMontyException::new(exc))),\n                            };\n                        }\n                        RunProgress::NameLookup(lookup) => {\n                            let result = resolve_name_lookup(external_functions.as_ref(), &lookup.name)?;\n                            progress = match lookup.resume(result, print_output.reborrow()) {\n                                Ok(p) => p,\n                                Err(exc) => return Ok(Either::B(JsMontyException::new(exc))),\n                            };\n                        }\n                        RunProgress::ResolveFutures(_) => {\n                            return Err(Error::from_reason(\n                                \"Async futures are not supported in synchronous run(). Use start() for async execution.\",\n                            ));\n                        }\n                        RunProgress::OsCall(OsCall { function, .. }) => {\n                            return Ok(Either::B(JsMontyException::new(MontyException::new(\n                                ExcType::NotImplementedError,\n                                Some(format!(\"OS function '{function}' not implemented\")),\n                            ))));\n                        }\n                    }\n                }\n            }};\n        }\n\n        if let Some(limits) = limits {\n            let tracker = LimitedTracker::new(limits.into());\n            run_loop!(tracker)\n        } else {\n            run_loop!(NoLimitTracker)\n        }\n    }\n\n    /// Starts execution and returns a snapshot (paused at external call or name lookup),\n    /// completion, or error.\n    ///\n    /// This method enables iterative execution where code pauses at external function\n    /// calls or name lookups, allowing the host to provide return values before resuming.\n    ///\n    /// @param options - Execution options (inputs, limits)\n    /// @returns MontySnapshot if paused at function call, MontyNameLookup if paused at\n    ///   name lookup, MontyComplete if done, or MontyException if failed\n    #[napi]\n    pub fn start<'env>(\n        &self,\n        env: &'env Env,\n        options: Option<StartOptions<'env>>,\n    ) -> Result<Either4<MontySnapshot, MontyNameLookup, MontyComplete, JsMontyException>> {\n        let options = options.unwrap_or_default();\n        let input_values = self.extract_input_values(options.inputs, *env)?;\n\n        // Clone the runner since start() consumes it - allows reuse of the parsed code\n        let runner = self.runner.clone();\n\n        // Build print writer and capture the callback ref for the snapshot\n        let mut print_cb;\n        let print_writer = match &options.print_callback {\n            Some(func) => {\n                print_cb = CallbackStringPrint::new_js(env, func)?;\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n        let print_callback_ref = options.print_callback.as_ref().map(Function::create_ref).transpose()?;\n\n        // Start execution with appropriate tracker\n        if let Some(limits) = options.limits {\n            let tracker = LimitedTracker::new(limits.into());\n            let progress = match runner.start(input_values, tracker, print_writer) {\n                Ok(p) => p,\n                Err(exc) => return Ok(Either4::D(JsMontyException::new(exc))),\n            };\n            Ok(progress_to_result(progress, print_callback_ref, self.script_name()))\n        } else {\n            let tracker = NoLimitTracker;\n            let progress = match runner.start(input_values, tracker, print_writer) {\n                Ok(p) => p,\n                Err(exc) => return Ok(Either4::D(JsMontyException::new(exc))),\n            };\n            Ok(progress_to_result(progress, print_callback_ref, self.script_name()))\n        }\n    }\n\n    /// Serializes the Monty instance to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `Monty.load()`.\n    /// This allows caching parsed code to avoid re-parsing on subsequent runs.\n    ///\n    /// @returns Buffer containing the serialized Monty instance\n    #[napi]\n    pub fn dump(&self) -> Result<Buffer> {\n        let serialized = SerializedMonty {\n            runner: self.runner.clone(),\n            script_name: self.script_name.clone(),\n            input_names: self.input_names.clone(),\n        };\n        let bytes =\n            postcard::to_allocvec(&serialized).map_err(|e| Error::from_reason(format!(\"Serialization failed: {e}\")))?;\n        Ok(Buffer::from(bytes))\n    }\n\n    /// Deserializes a Monty instance from binary format.\n    ///\n    /// @param data - The serialized Monty data from `dump()`\n    /// @returns A new Monty instance\n    #[napi(factory)]\n    pub fn load(data: Buffer) -> Result<Self> {\n        let serialized: SerializedMonty =\n            postcard::from_bytes(&data).map_err(|e| Error::from_reason(format!(\"Deserialization failed: {e}\")))?;\n\n        Ok(Self {\n            runner: serialized.runner,\n            script_name: serialized.script_name,\n            input_names: serialized.input_names,\n        })\n    }\n\n    /// Returns the script name.\n    #[napi(getter)]\n    pub fn script_name(&self) -> String {\n        self.script_name.clone()\n    }\n\n    /// Returns the input variable names.\n    #[napi(getter)]\n    pub fn inputs(&self) -> Vec<String> {\n        self.input_names.clone()\n    }\n\n    /// Returns a string representation of the Monty instance.\n    #[napi]\n    pub fn repr(&self) -> String {\n        use std::fmt::Write;\n        let lines = self.runner.code().lines().count();\n        let mut s = format!(\n            \"Monty(<{} line{} of code>, scriptName='{}'\",\n            lines,\n            if lines == 1 { \"\" } else { \"s\" },\n            self.script_name\n        );\n        if !self.input_names.is_empty() {\n            write!(s, \", inputs={:?}\", self.input_names).unwrap();\n        }\n        s.push(')');\n        s\n    }\n\n    /// Extracts input values from the JS Object in the order they were declared.\n    fn extract_input_values(&self, inputs: Option<Object<'_>>, env: Env) -> Result<Vec<MontyObject>> {\n        extract_input_values_in_order(&self.input_names, inputs, env)\n    }\n}\n\n/// Performs type checking on the code and returns the error object if there are type errors.\n///\n/// Returns `None` if type checking passes, or `Some(MontyTypingError)` if there are errors.\nfn run_type_check_result(code: &str, script_name: &str, prefix_code: Option<&str>) -> Result<Option<MontyTypingError>> {\n    let source_code: Cow<str> = if let Some(prefix_code) = prefix_code {\n        format!(\"{prefix_code}\\n{code}\").into()\n    } else {\n        code.into()\n    };\n\n    let source_file = SourceFile::new(&source_code, script_name);\n    let result =\n        type_check(&source_file, None).map_err(|e| Error::from_reason(format!(\"Type checking failed: {e}\")))?;\n\n    Ok(result.map(MontyTypingError::from_failure))\n}\n\n// =============================================================================\n// MontyRepl - Incremental no-replay REPL session\n// =============================================================================\n\n/// REPL state holder for napi interoperability.\n///\n/// `napi` classes cannot be generic, so this enum stores REPL sessions for both\n/// resource tracker variants.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nenum EitherRepl {\n    NoLimit(CoreMontyRepl<NoLimitTracker>),\n    Limited(CoreMontyRepl<LimitedTracker>),\n}\n\n/// Options for creating a new `MontyRepl` instance.\n///\n/// Controls the script name shown in tracebacks and optional resource limits\n/// that apply to all subsequent `feed()` calls.\n#[napi(object)]\n#[derive(Default)]\npub struct MontyReplOptions {\n    /// Name used in tracebacks and error messages. Default: 'main.py'\n    pub script_name: Option<String>,\n    /// Resource limits configuration applied to all snippet executions.\n    pub limits: Option<JsResourceLimits>,\n}\n\n/// Stateful no-replay REPL session.\n///\n/// Create with `new MontyRepl()` then call `feed()` to execute snippets\n/// incrementally against persistent heap and namespace state.\n#[napi]\npub struct MontyRepl {\n    repl: EitherRepl,\n    script_name: String,\n}\n\n#[napi]\nimpl MontyRepl {\n    /// Creates an empty REPL session ready to receive snippets via `feed()`.\n    ///\n    /// No code is parsed or executed at construction time — all execution\n    /// is driven through `feed()`.\n    ///\n    /// @param options - Optional configuration (scriptName, limits)\n    #[napi(constructor)]\n    #[must_use]\n    pub fn new(options: Option<MontyReplOptions>) -> Self {\n        let options = options.unwrap_or_default();\n        let script_name = options.script_name.unwrap_or_else(|| \"main.py\".to_string());\n\n        let repl = if let Some(limits) = options.limits {\n            let tracker = LimitedTracker::new(limits.into());\n            EitherRepl::Limited(CoreMontyRepl::new(&script_name, tracker))\n        } else {\n            EitherRepl::NoLimit(CoreMontyRepl::new(&script_name, NoLimitTracker))\n        };\n\n        Self { repl, script_name }\n    }\n\n    /// Returns the script name for this REPL session.\n    #[napi(getter)]\n    #[must_use]\n    pub fn script_name(&self) -> String {\n        self.script_name.clone()\n    }\n\n    /// Executes one incremental snippet against persistent REPL state.\n    #[napi]\n    pub fn feed<'env>(\n        &mut self,\n        env: &'env Env,\n        code: String,\n    ) -> Result<Either<JsMontyObject<'env>, JsMontyException>> {\n        let output = match &mut self.repl {\n            EitherRepl::NoLimit(repl) => repl.feed_run(&code, vec![], PrintWriter::Stdout),\n            EitherRepl::Limited(repl) => repl.feed_run(&code, vec![], PrintWriter::Stdout),\n        };\n\n        match output {\n            Ok(value) => Ok(Either::A(monty_to_js(&value, env)?)),\n            Err(exc) => Ok(Either::B(JsMontyException::new(exc))),\n        }\n    }\n\n    /// Serializes this REPL session to bytes.\n    #[napi]\n    pub fn dump(&self) -> Result<Buffer> {\n        let serialized = SerializedRepl {\n            repl: &self.repl,\n            script_name: &self.script_name,\n        };\n        let bytes =\n            postcard::to_allocvec(&serialized).map_err(|e| Error::from_reason(format!(\"Serialization failed: {e}\")))?;\n        Ok(Buffer::from(bytes))\n    }\n\n    /// Restores a REPL session from bytes produced by `dump()`.\n    #[napi(factory)]\n    pub fn load(data: Buffer) -> Result<Self> {\n        let serialized: SerializedReplOwned =\n            postcard::from_bytes(&data).map_err(|e| Error::from_reason(format!(\"Deserialization failed: {e}\")))?;\n        Ok(Self {\n            repl: serialized.repl,\n            script_name: serialized.script_name,\n        })\n    }\n\n    /// Returns a string representation of the REPL session.\n    #[napi]\n    #[must_use]\n    pub fn repr(&self) -> String {\n        format!(\"MontyRepl(scriptName='{}')\", self.script_name)\n    }\n}\n\n/// Fully resolved creation options shared by `Monty` and `MontyRepl`.\n///\n/// This keeps parsing/type-checking defaults consistent across non-REPL and\n/// REPL entry points.\nstruct ResolvedMontyOptions {\n    script_name: String,\n    input_names: Vec<String>,\n    do_type_check: bool,\n    type_check_prefix_code: Option<String>,\n}\n\n/// Normalizes optional JS-facing creation options into concrete defaults.\nfn resolve_monty_options(options: Option<MontyOptions>) -> ResolvedMontyOptions {\n    let options = options.unwrap_or(MontyOptions {\n        script_name: None,\n        inputs: None,\n        type_check: None,\n        type_check_prefix_code: None,\n    });\n\n    ResolvedMontyOptions {\n        script_name: options.script_name.unwrap_or_else(|| \"main.py\".to_string()),\n        input_names: options.inputs.unwrap_or_default(),\n        do_type_check: options.type_check.unwrap_or(false),\n        type_check_prefix_code: options.type_check_prefix_code,\n    }\n}\n\n/// Extracts input values in declaration order from a JS object.\n///\n/// This helper is shared by regular `Monty` execution and direct REPL creation\n/// so both paths perform identical input validation.\nfn extract_input_values_in_order(\n    input_names: &[String],\n    inputs: Option<Object<'_>>,\n    env: Env,\n) -> Result<Vec<MontyObject>> {\n    if input_names.is_empty() {\n        if inputs.is_some() {\n            return Err(Error::from_reason(\n                \"No input variables declared but inputs object was provided\",\n            ));\n        }\n        return Ok(vec![]);\n    }\n\n    let Some(inputs) = inputs else {\n        return Err(Error::from_reason(format!(\"Missing required inputs: {input_names:?}\")));\n    };\n\n    input_names\n        .iter()\n        .map(|name| {\n            if !inputs.has_named_property(name)? {\n                return Err(Error::from_reason(format!(\"Missing required input: '{name}'\")));\n            }\n            let value: Unknown = inputs.get_named_property(name)?;\n            js_to_monty(value, env)\n        })\n        .collect()\n}\n\n// =============================================================================\n// EitherSnapshot - Internal enum to handle generic resource tracker types\n// =============================================================================\n\n/// Runtime execution snapshot, holds a `FunctionCall` for either resource tracker variant\n/// since napi structs can't be generic.\n///\n/// Used internally by `MontySnapshot` to store execution state.\n/// The `Done` variant indicates the snapshot has been consumed.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nenum EitherSnapshot {\n    NoLimit(FunctionCall<NoLimitTracker>),\n    Limited(FunctionCall<LimitedTracker>),\n    /// Sentinel indicating the snapshot has been consumed via `resume()`.\n    Done,\n}\n\n// =============================================================================\n// MontySnapshot - Paused execution at an external function call\n// =============================================================================\n\n/// Represents paused execution waiting for an external function call return value.\n///\n/// Contains information about the pending external function call and allows\n/// resuming execution with the return value or an exception.\n#[napi]\npub struct MontySnapshot {\n    /// The execution state that can be resumed.\n    snapshot: EitherSnapshot,\n    /// Name of the script being executed.\n    script_name: String,\n    /// The name of the external function being called.\n    function_name: String,\n    /// The positional arguments passed to the function (stored as MontyObject for serialization).\n    args: Vec<MontyObject>,\n    /// The keyword arguments passed to the function (stored as MontyObject pairs for serialization).\n    kwargs: Vec<(MontyObject, MontyObject)>,\n    /// Optional print callback function.\n    print_callback: Option<JsPrintCallbackRef>,\n}\n\n/// Options for resuming execution.\n#[napi(object)]\npub struct ResumeOptions<'env> {\n    /// The value to return from the external function call.\n    pub return_value: Option<Unknown<'env>>,\n    /// An exception to raise in the interpreter.\n    /// Format: { type: string, message: string }\n    pub exception: Option<ExceptionInput>,\n}\n\n/// Input for raising an exception during resume.\n#[napi(object)]\npub struct ExceptionInput {\n    /// The exception type name (e.g., \"ValueError\").\n    pub r#type: String,\n    /// The exception message.\n    pub message: String,\n}\n\n/// Options for loading a serialized snapshot.\n#[napi(object)]\npub struct SnapshotLoadOptions<'env> {\n    /// Optional print callback function.\n    pub print_callback: Option<JsPrintCallback<'env>>,\n    // Future: could add dataclass-like registry support\n}\n\n#[napi]\nimpl MontySnapshot {\n    /// Returns the name of the script being executed.\n    #[napi(getter)]\n    pub fn script_name(&self) -> String {\n        self.script_name.clone()\n    }\n\n    /// Returns the name of the external function being called.\n    #[napi(getter)]\n    pub fn function_name(&self) -> String {\n        self.function_name.clone()\n    }\n\n    /// Returns the positional arguments passed to the external function.\n    #[napi(getter)]\n    pub fn args<'env>(&self, env: &'env Env) -> Result<Vec<JsMontyObject<'env>>> {\n        self.args.iter().map(|obj| monty_to_js(obj, env)).collect()\n    }\n\n    /// Returns the keyword arguments passed to the external function as an object.\n    #[napi(getter)]\n    pub fn kwargs<'env>(&self, env: &'env Env) -> Result<Object<'env>> {\n        let mut obj = Object::new(env)?;\n        for (k, v) in &self.kwargs {\n            // Keys should be strings\n            let key = match k {\n                MontyObject::String(s) => s.clone(),\n                _ => format!(\"{k:?}\"),\n            };\n            let js_value = monty_to_js(v, env)?;\n            obj.set_named_property(&key, js_value)?;\n        }\n        Ok(obj)\n    }\n\n    /// Resumes execution with either a return value or an exception.\n    ///\n    /// Exactly one of `returnValue` or `exception` must be provided.\n    ///\n    /// @param options - Object with either `returnValue` or `exception`\n    /// @returns MontySnapshot if paused at function call, MontyNameLookup if paused at\n    ///   name lookup, MontyComplete if done, or MontyException if failed\n    #[napi]\n    pub fn resume<'env>(\n        &mut self,\n        env: &'env Env,\n        options: ResumeOptions<'env>,\n    ) -> Result<Either4<Self, MontyNameLookup, MontyComplete, JsMontyException>> {\n        // Validate that exactly one of returnValue or exception is provided\n        let external_result = match (options.return_value, options.exception) {\n            (Some(value), None) => {\n                let monty_value = js_to_monty(value, *env)?;\n                ExtFunctionResult::Return(monty_value)\n            }\n            (None, Some(exc)) => {\n                let monty_exc = MontyException::new(string_to_exc_type(&exc.r#type)?, Some(exc.message));\n                ExtFunctionResult::Error(monty_exc)\n            }\n            (Some(_), Some(_)) => {\n                return Err(Error::from_reason(\n                    \"resume() accepts either returnValue or exception, not both\",\n                ));\n            }\n            (None, None) => {\n                return Err(Error::from_reason(\"resume() requires either returnValue or exception\"));\n            }\n        };\n\n        // Take the snapshot, replacing with Done\n        let snapshot = std::mem::replace(&mut self.snapshot, EitherSnapshot::Done);\n\n        // Take the print callback\n        // This is necessary to move out of `&mut self` to please the borrow checker.\n        // Unless the entire snapshot generator is refactored we have to do this.\n        let print_callback = std::mem::take(&mut self.print_callback);\n\n        // Build print writer from the callback ref\n        let mut print_cb;\n        let print_writer = match &print_callback {\n            Some(func) => {\n                print_cb = CallbackStringPrint::new_js_ref(env, func)?;\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n\n        // Resume execution based on the snapshot type\n        match snapshot {\n            EitherSnapshot::NoLimit(call) => {\n                let progress = match call.resume(external_result, print_writer) {\n                    Ok(p) => p,\n                    Err(exc) => return Ok(Either4::D(JsMontyException::new(exc))),\n                };\n                Ok(progress_to_result(progress, print_callback, self.script_name.clone()))\n            }\n            EitherSnapshot::Limited(call) => {\n                let progress = match call.resume(external_result, print_writer) {\n                    Ok(p) => p,\n                    Err(exc) => return Ok(Either4::D(JsMontyException::new(exc))),\n                };\n                Ok(progress_to_result(progress, print_callback, self.script_name.clone()))\n            }\n            EitherSnapshot::Done => Err(Error::from_reason(\"Snapshot has already been resumed\")),\n        }\n    }\n\n    /// Serializes the MontySnapshot to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `MontySnapshot.load()`.\n    /// This allows suspending execution and resuming later, potentially in a different process.\n    ///\n    /// @returns Buffer containing the serialized snapshot\n    #[napi]\n    pub fn dump(&self) -> Result<Buffer> {\n        if matches!(self.snapshot, EitherSnapshot::Done) {\n            return Err(Error::from_reason(\"Cannot dump snapshot that has already been resumed\"));\n        }\n\n        let serialized = SerializedSnapshot {\n            snapshot: &self.snapshot,\n            script_name: &self.script_name,\n            function_name: &self.function_name,\n            args: &self.args,\n            kwargs: &self.kwargs,\n        };\n\n        let bytes =\n            postcard::to_allocvec(&serialized).map_err(|e| Error::from_reason(format!(\"Serialization failed: {e}\")))?;\n        Ok(Buffer::from(bytes))\n    }\n\n    /// Deserializes a MontySnapshot from binary format.\n    ///\n    /// @param data - The serialized snapshot data from `dump()`\n    /// @param options - Optional load options (reserved for future use)\n    /// @returns A new MontySnapshot instance\n    #[napi(factory)]\n    pub fn load(data: Buffer, options: Option<SnapshotLoadOptions>) -> Result<Self> {\n        let serialized: SerializedSnapshotOwned =\n            postcard::from_bytes(&data).map_err(|e| Error::from_reason(format!(\"Deserialization failed: {e}\")))?;\n\n        Ok(Self {\n            snapshot: serialized.snapshot,\n            script_name: serialized.script_name,\n            function_name: serialized.function_name,\n            args: serialized.args,\n            kwargs: serialized.kwargs,\n            print_callback: options\n                .as_ref()\n                .and_then(|t| t.print_callback.as_ref())\n                .map(Function::create_ref)\n                .transpose()?,\n        })\n    }\n\n    /// Returns a string representation of the MontySnapshot.\n    #[napi]\n    pub fn repr(&self) -> String {\n        format!(\n            \"MontySnapshot(scriptName='{}', functionName='{}', args={:?}, kwargs={:?})\",\n            self.script_name, self.function_name, self.args, self.kwargs\n        )\n    }\n}\n\n// =============================================================================\n// MontyComplete - Completed execution\n// =============================================================================\n\n/// Represents completed execution with a final output value.\n///\n/// The output value is stored as a `MontyObject` internally and converted to JS on access.\n#[napi]\npub struct MontyComplete {\n    /// The final output value from the executed code.\n    output_value: MontyObject,\n}\n\n#[napi]\nimpl MontyComplete {\n    /// Returns the final output value from the executed code.\n    #[napi(getter)]\n    pub fn output<'env>(&self, env: &'env Env) -> Result<JsMontyObject<'env>> {\n        monty_to_js(&self.output_value, env)\n    }\n\n    /// Returns a string representation of the MontyComplete.\n    #[napi]\n    #[must_use]\n    pub fn repr(&self) -> String {\n        format!(\"MontyComplete(output={:?})\", self.output_value)\n    }\n}\n\n// =============================================================================\n// EitherLookupSnapshot - Internal enum for NameLookup tracker variants\n// =============================================================================\n\n/// Runtime execution snapshot, holds a `NameLookup` for either resource tracker variant\n/// since napi structs can't be generic.\n///\n/// The `Done` variant indicates the snapshot has been consumed.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nenum EitherLookupSnapshot {\n    NoLimit(NameLookup<NoLimitTracker>),\n    Limited(NameLookup<LimitedTracker>),\n    /// Sentinel indicating the snapshot has been consumed via `resume()`.\n    Done,\n}\n\n/// Trait to convert a typed `NameLookup` into `EitherLookupSnapshot`.\ntrait FromLookupSnapshot<T: ResourceTracker> {\n    /// Wraps a name-lookup snapshot.\n    fn from_lookup(lookup: NameLookup<T>) -> Self;\n}\n\nimpl FromLookupSnapshot<NoLimitTracker> for EitherLookupSnapshot {\n    fn from_lookup(lookup: NameLookup<NoLimitTracker>) -> Self {\n        Self::NoLimit(lookup)\n    }\n}\n\nimpl FromLookupSnapshot<LimitedTracker> for EitherLookupSnapshot {\n    fn from_lookup(lookup: NameLookup<LimitedTracker>) -> Self {\n        Self::Limited(lookup)\n    }\n}\n\n// =============================================================================\n// MontyNameLookup - Paused execution at a name lookup\n// =============================================================================\n\n/// Represents paused execution waiting for a name to be resolved.\n///\n/// The host should check if the variable name corresponds to a known value\n/// (e.g., an external function). Call `resume()` with the value to continue\n/// execution, or call `resume()` with no value to raise `NameError`.\n#[napi]\npub struct MontyNameLookup {\n    /// The execution state that can be resumed.\n    snapshot: EitherLookupSnapshot,\n    /// Name of the script being executed.\n    script_name: String,\n    /// The name of the variable being looked up.\n    variable_name: String,\n    /// Optional print callback function.\n    print_callback: Option<JsPrintCallbackRef>,\n}\n\n/// Options for resuming execution from a name lookup.\n///\n/// If `value` is provided, the name resolves to that value and execution continues.\n/// If `value` is omitted or undefined, the VM raises a `NameError`.\n#[napi(object)]\npub struct NameLookupResumeOptions<'env> {\n    /// The value to provide for the name.\n    pub value: Option<Unknown<'env>>,\n}\n\n/// Options for loading a serialized name lookup snapshot.\n#[napi(object)]\npub struct NameLookupLoadOptions<'env> {\n    /// Optional print callback function.\n    pub print_callback: Option<JsPrintCallback<'env>>,\n}\n\n#[napi]\nimpl MontyNameLookup {\n    /// Returns the name of the script being executed.\n    #[napi(getter)]\n    pub fn script_name(&self) -> String {\n        self.script_name.clone()\n    }\n\n    /// Returns the name of the variable being looked up.\n    #[napi(getter)]\n    pub fn variable_name(&self) -> String {\n        self.variable_name.clone()\n    }\n\n    /// Resumes execution after resolving the name lookup.\n    ///\n    /// If `value` is provided, the name resolves to that value and execution continues.\n    /// If `value` is omitted or undefined, the VM raises a `NameError`.\n    ///\n    /// @param options - Optional object with `value` to resolve the name to\n    /// @returns MontySnapshot if paused at function call, MontyNameLookup if paused at\n    ///   another name lookup, MontyComplete if done, or MontyException if failed\n    #[napi]\n    pub fn resume<'env>(\n        &mut self,\n        env: &'env Env,\n        options: Option<NameLookupResumeOptions<'env>>,\n    ) -> Result<Either4<MontySnapshot, Self, MontyComplete, JsMontyException>> {\n        let lookup_result = match options.and_then(|opts| opts.value) {\n            Some(value) => {\n                let monty_value = js_to_monty(value, *env)?;\n                NameLookupResult::Value(monty_value)\n            }\n            None => NameLookupResult::Undefined,\n        };\n\n        // Take the snapshot, replacing with Done\n        let snapshot = std::mem::replace(&mut self.snapshot, EitherLookupSnapshot::Done);\n\n        // Take the print callback\n        let print_callback = std::mem::take(&mut self.print_callback);\n\n        // Build print writer from the callback ref\n        let mut print_cb;\n        let print_writer = match &print_callback {\n            Some(func) => {\n                print_cb = CallbackStringPrint::new_js_ref(env, func)?;\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n\n        match snapshot {\n            EitherLookupSnapshot::NoLimit(lookup) => {\n                let progress = match lookup.resume(lookup_result, print_writer) {\n                    Ok(p) => p,\n                    Err(exc) => return Ok(Either4::D(JsMontyException::new(exc))),\n                };\n                Ok(progress_to_result(progress, print_callback, self.script_name.clone()))\n            }\n            EitherLookupSnapshot::Limited(lookup) => {\n                let progress = match lookup.resume(lookup_result, print_writer) {\n                    Ok(p) => p,\n                    Err(exc) => return Ok(Either4::D(JsMontyException::new(exc))),\n                };\n                Ok(progress_to_result(progress, print_callback, self.script_name.clone()))\n            }\n            EitherLookupSnapshot::Done => Err(Error::from_reason(\"Name lookup has already been resumed\")),\n        }\n    }\n\n    /// Serializes the MontyNameLookup to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `MontyNameLookup.load()`.\n    ///\n    /// @returns Buffer containing the serialized name lookup snapshot\n    #[napi]\n    pub fn dump(&self) -> Result<Buffer> {\n        if matches!(self.snapshot, EitherLookupSnapshot::Done) {\n            return Err(Error::from_reason(\n                \"Cannot dump name lookup that has already been resumed\",\n            ));\n        }\n\n        let serialized = SerializedNameLookup {\n            snapshot: &self.snapshot,\n            script_name: &self.script_name,\n            variable_name: &self.variable_name,\n        };\n\n        let bytes =\n            postcard::to_allocvec(&serialized).map_err(|e| Error::from_reason(format!(\"Serialization failed: {e}\")))?;\n        Ok(Buffer::from(bytes))\n    }\n\n    /// Deserializes a MontyNameLookup from binary format.\n    ///\n    /// @param data - The serialized data from `dump()`\n    /// @param options - Optional load options\n    /// @returns A new MontyNameLookup instance\n    #[napi(factory)]\n    pub fn load(data: Buffer, options: Option<NameLookupLoadOptions>) -> Result<Self> {\n        let serialized: SerializedNameLookupOwned =\n            postcard::from_bytes(&data).map_err(|e| Error::from_reason(format!(\"Deserialization failed: {e}\")))?;\n\n        Ok(Self {\n            snapshot: serialized.snapshot,\n            script_name: serialized.script_name,\n            variable_name: serialized.variable_name,\n            print_callback: options\n                .as_ref()\n                .and_then(|t| t.print_callback.as_ref())\n                .map(Function::create_ref)\n                .transpose()?,\n        })\n    }\n\n    /// Returns a string representation of the MontyNameLookup.\n    #[napi]\n    pub fn repr(&self) -> String {\n        format!(\n            \"MontyNameLookup(scriptName='{}', variableName='{}')\",\n            self.script_name, self.variable_name\n        )\n    }\n}\n\n// Function type for JS callback used in `CallbackStringPrint`.\ntype JsPrintCallback<'env> = Function<'env, FnArgs<(&'static str, String)>, ()>;\ntype JsPrintCallbackRef = FunctionRef<FnArgs<(&'static str, String)>, ()>;\n\n/// A `PrintWriter` implementation that calls a javascript callback for each print output.\n///\n/// This structure internally holds a `napi::Function`.\npub struct CallbackStringPrint<'env>(JsPrintCallback<'env>);\n\nimpl<'env> CallbackStringPrint<'env> {\n    /// Creates a new `CallbackStringPrint` from a `JsFunction`.\n    pub fn new_js(env: &'env Env, func: &JsPrintCallback<'env>) -> napi::Result<Self> {\n        Ok(Self(func.create_ref()?.borrow_back(env)?))\n    }\n\n    /// Creates a new printer from a function reference.\n    ///\n    /// This will re-borrow the function reference for use in printing.\n    pub fn new_js_ref(env: &'env Env, func: &JsPrintCallbackRef) -> napi::Result<Self> {\n        Ok(Self(func.borrow_back(env)?))\n    }\n}\n\nimpl PrintWriterCallback for CallbackStringPrint<'_> {\n    fn stdout_write(&mut self, output: Cow<'_, str>) -> std::result::Result<(), MontyException> {\n        self.0\n            .call((\"stdout\", output.as_ref().to_owned()).into())\n            .map_err(exc_js_to_monty)?;\n        Ok(())\n    }\n\n    fn stdout_push(&mut self, end: char) -> std::result::Result<(), MontyException> {\n        self.0\n            .call((\"stdout\", end.to_string()).into())\n            .map_err(exc_js_to_monty)?;\n        Ok(())\n    }\n}\n\n// =============================================================================\n// Helper functions for progress conversion\n// =============================================================================\n\n/// Converts a `RunProgress` to either a `MontySnapshot`, `MontyNameLookup`,\n/// `MontyComplete`, or `JsMontyException`.\n///\n/// `NameLookup` events are surfaced to the host as `MontyNameLookup` instances,\n/// allowing the host to decide how to resolve each name (or let the VM raise `NameError`).\n///\n/// For progress types that are not yet supported in the JS bindings (`ResolveFutures`, `OsCall`),\n/// returns a `JsMontyException` with `NotImplementedError` instead of panicking, matching\n/// the Python bindings behavior.\nfn progress_to_result<T>(\n    progress: RunProgress<T>,\n    print_callback: Option<JsPrintCallbackRef>,\n    script_name: String,\n) -> Either4<MontySnapshot, MontyNameLookup, MontyComplete, JsMontyException>\nwhere\n    T: ResourceTracker + serde::Serialize + serde::de::DeserializeOwned,\n    EitherSnapshot: FromSnapshot<T>,\n    EitherLookupSnapshot: FromLookupSnapshot<T>,\n{\n    match progress {\n        RunProgress::Complete(result) => Either4::C(MontyComplete { output_value: result }),\n        RunProgress::FunctionCall(call) => {\n            let function_name = call.function_name.clone();\n            let args = call.args.clone();\n            let kwargs = call.kwargs.clone();\n            Either4::A(MontySnapshot {\n                snapshot: EitherSnapshot::from_snapshot(call),\n                script_name,\n                function_name,\n                args,\n                kwargs,\n                print_callback,\n            })\n        }\n        RunProgress::NameLookup(lookup) => {\n            let variable_name = lookup.name.clone();\n            Either4::B(MontyNameLookup {\n                snapshot: EitherLookupSnapshot::from_lookup(lookup),\n                script_name,\n                variable_name,\n                print_callback,\n            })\n        }\n        RunProgress::ResolveFutures(_) => Either4::D(JsMontyException::new(MontyException::new(\n            ExcType::NotImplementedError,\n            Some(\"Async futures (ResolveFutures) are not yet supported in the JS bindings\".to_owned()),\n        ))),\n        RunProgress::OsCall(OsCall { function, .. }) => Either4::D(JsMontyException::new(MontyException::new(\n            ExcType::NotImplementedError,\n            Some(format!(\"OS function '{function}' not implemented\")),\n        ))),\n    }\n}\n\n/// Trait to convert a typed `FunctionCall` into `EitherSnapshot`.\ntrait FromSnapshot<T: ResourceTracker> {\n    /// Wraps a function-call snapshot.\n    fn from_snapshot(call: FunctionCall<T>) -> Self;\n}\n\nimpl FromSnapshot<NoLimitTracker> for EitherSnapshot {\n    fn from_snapshot(call: FunctionCall<NoLimitTracker>) -> Self {\n        Self::NoLimit(call)\n    }\n}\n\nimpl FromSnapshot<LimitedTracker> for EitherSnapshot {\n    fn from_snapshot(call: FunctionCall<LimitedTracker>) -> Self {\n        Self::Limited(call)\n    }\n}\n\n/// Converts a string exception type to `ExcType`.\nfn string_to_exc_type(type_name: &str) -> Result<ExcType> {\n    type_name\n        .parse()\n        .map_err(|_| Error::from_reason(format!(\"Invalid exception type: '{type_name}'\")))\n}\n\n// =============================================================================\n// Serialization types\n// =============================================================================\n\n/// Serialization wrapper for `Monty` that includes all fields needed for reconstruction.\n#[derive(serde::Serialize, serde::Deserialize)]\nstruct SerializedMonty {\n    runner: MontyRun,\n    script_name: String,\n    input_names: Vec<String>,\n}\n\n/// Serialization wrapper for `MontyRepl` using borrowed references.\n#[derive(serde::Serialize)]\nstruct SerializedRepl<'a> {\n    repl: &'a EitherRepl,\n    script_name: &'a str,\n}\n\n/// Owned version of `SerializedRepl` for deserialization.\n#[derive(serde::Deserialize)]\nstruct SerializedReplOwned {\n    repl: EitherRepl,\n    script_name: String,\n}\n\n/// Serialization wrapper for `MontySnapshot` using borrowed references.\n#[derive(serde::Serialize)]\nstruct SerializedSnapshot<'a> {\n    snapshot: &'a EitherSnapshot,\n    script_name: &'a str,\n    function_name: &'a str,\n    args: &'a [MontyObject],\n    kwargs: &'a [(MontyObject, MontyObject)],\n}\n\n/// Owned version of `SerializedSnapshot` for deserialization.\n#[derive(serde::Deserialize)]\nstruct SerializedSnapshotOwned {\n    snapshot: EitherSnapshot,\n    script_name: String,\n    function_name: String,\n    args: Vec<MontyObject>,\n    kwargs: Vec<(MontyObject, MontyObject)>,\n}\n\n/// Serialization wrapper for `MontyNameLookup` using borrowed references.\n#[derive(serde::Serialize)]\nstruct SerializedNameLookup<'a> {\n    snapshot: &'a EitherLookupSnapshot,\n    script_name: &'a str,\n    variable_name: &'a str,\n}\n\n/// Owned version of `SerializedNameLookup` for deserialization.\n#[derive(serde::Deserialize)]\nstruct SerializedNameLookupOwned {\n    snapshot: EitherLookupSnapshot,\n    script_name: String,\n    variable_name: String,\n}\n\n// =============================================================================\n// External function support\n// =============================================================================\n\n/// Calls a JavaScript external function and returns the result.\n///\n/// Converts args/kwargs from Monty format, calls the JS function,\n/// and converts the result back to Monty format (or an exception).\nfn call_external_function(\n    env: &Env,\n    external_functions: Option<&Object<'_>>,\n    function_name: &str,\n    args: &[MontyObject],\n    kwargs: &[(MontyObject, MontyObject)],\n) -> Result<ExtFunctionResult> {\n    // Get the external functions dict, or error if not provided\n    let functions = external_functions.ok_or_else(|| {\n        Error::from_reason(format!(\n            \"External function '{function_name}' called but no externalFunctions provided\"\n        ))\n    })?;\n\n    // Look up the function by name\n    if !functions.has_named_property(function_name)? {\n        // Return a NameError exception — matches Python's behavior for undefined names\n        let exc = MontyException::new(\n            ExcType::NameError,\n            Some(format!(\"name '{function_name}' is not defined\")),\n        );\n        return Ok(ExtFunctionResult::Error(exc));\n    }\n\n    let callable: Unknown = functions.get_named_property(function_name)?;\n\n    // Convert positional arguments to JS\n    let mut js_args: Vec<sys::napi_value> = Vec::with_capacity(args.len() + 1);\n    for arg in args {\n        js_args.push(monty_to_js(arg, env)?.raw());\n    }\n\n    // If we have kwargs, add them as a final object argument\n    if !kwargs.is_empty() {\n        let mut kwargs_obj = Object::new(env)?;\n        for (key, value) in kwargs {\n            let key_str = match key {\n                MontyObject::String(s) => s.clone(),\n                _ => format!(\"{key:?}\"),\n            };\n            kwargs_obj.set_named_property(&key_str, monty_to_js(value, env)?)?;\n        }\n        js_args.push(kwargs_obj.raw());\n    }\n\n    // Get undefined for the 'this' argument\n    let mut undefined_raw = std::ptr::null_mut();\n    // SAFETY: [DH] - all arguments are valid and result is valid on success\n    unsafe {\n        sys::napi_get_undefined(env.raw(), &raw mut undefined_raw);\n    }\n\n    // Call the function using raw napi\n    let mut result_raw = std::ptr::null_mut();\n    // SAFETY: [DH] - all arguments are valid and result is valid on success\n    let status = unsafe {\n        sys::napi_call_function(\n            env.raw(),\n            undefined_raw, // this = undefined\n            callable.raw(),\n            js_args.len(),\n            js_args.as_ptr(),\n            &raw mut result_raw,\n        )\n    };\n\n    if status != sys::Status::napi_ok {\n        // An error occurred - get the pending exception\n        let mut is_exception = false;\n        // SAFETY: [DH] - all arguments are valid\n        unsafe { sys::napi_is_exception_pending(env.raw(), &raw mut is_exception) };\n\n        if is_exception {\n            let mut exception_raw = std::ptr::null_mut();\n            // SAFETY: [DH] - all arguments are valid and exception_raw is valid on success\n            let status = unsafe { sys::napi_get_and_clear_last_exception(env.raw(), &raw mut exception_raw) };\n\n            if status != sys::Status::napi_ok {\n                // Failed to get the exception - return a generic error\n                let exc = MontyException::new(\n                    ExcType::RuntimeError,\n                    Some(\"External function call failed and exception could not be retrieved\".to_string()),\n                );\n                return Ok(ExtFunctionResult::Error(exc));\n            }\n            let exception_obj = Object::from_raw(env.raw(), exception_raw);\n            let exc = extract_js_exception(exception_obj);\n            return Ok(ExtFunctionResult::Error(exc));\n        }\n\n        // Generic error\n        let exc = MontyException::new(ExcType::RuntimeError, Some(\"External function call failed\".to_string()));\n        return Ok(ExtFunctionResult::Error(exc));\n    }\n\n    // Convert the result back to Monty format\n    // SAFETY: [DH] - result_raw is valid on success\n    let result = unsafe { Unknown::from_raw_unchecked(env.raw(), result_raw) };\n    let monty_result = js_to_monty(result, *env)?;\n    Ok(ExtFunctionResult::Return(monty_result))\n}\n\n/// Extracts exception info from a JS exception object.\nfn extract_js_exception(exception_obj: Object<'_>) -> MontyException {\n    // Try to get the 'name' property (e.g., \"ValueError\")\n    let name: std::result::Result<String, _> = exception_obj.get_named_property(\"name\");\n    // Try to get the 'message' property\n    let message: std::result::Result<String, _> = exception_obj.get_named_property(\"message\");\n\n    let exc_type = name\n        .ok()\n        .and_then(|n| string_to_exc_type(&n).ok())\n        .unwrap_or(ExcType::RuntimeError);\n    let msg = message.ok();\n\n    MontyException::new(exc_type, msg)\n}\n\n/// Resolves a name lookup against the runtime external functions map.\n///\n/// If the name exists as a property on the external functions object, returns\n/// `NameLookupResult::Value` with a `Function` object. Otherwise returns\n/// `NameLookupResult::Undefined` so the VM raises `NameError`.\nfn resolve_name_lookup(external_functions: Option<&Object<'_>>, name: &str) -> Result<NameLookupResult> {\n    if let Some(functions) = external_functions {\n        if functions.has_named_property(name)? {\n            return Ok(NameLookupResult::Value(MontyObject::Function {\n                name: name.to_string(),\n                docstring: None, // TODO, can we do better?\n            }));\n        }\n    }\n    Ok(NameLookupResult::Undefined)\n}\n"
  },
  {
    "path": "crates/monty-js/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"strict\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true\n  },\n  \"include\": [\"wrapper.ts\"],\n  \"exclude\": [\"node_modules\", \"__test__\", \"benchmark\"]\n}\n"
  },
  {
    "path": "crates/monty-js/wrapper.ts",
    "content": "// Custom error classes that extend Error for proper JavaScript error handling.\n// These wrap the native Rust classes to provide instanceof support.\n\nimport type {\n  ExceptionInfo,\n  ExceptionInput,\n  Frame,\n  JsMontyObject,\n  MontyOptions,\n  NameLookupLoadOptions,\n  NameLookupResumeOptions,\n  ResourceLimits,\n  ResumeOptions,\n  RunOptions,\n  SnapshotLoadOptions,\n  StartOptions,\n} from './index.js'\n\nimport {\n  Monty as NativeMonty,\n  MontyRepl as NativeMontyRepl,\n  MontySnapshot as NativeMontySnapshot,\n  MontyNameLookup as NativeMontyNameLookup,\n  MontyComplete as NativeMontyComplete,\n  MontyException as NativeMontyException,\n  MontyTypingError as NativeMontyTypingError,\n} from './index.js'\n\nexport type {\n  MontyOptions,\n  RunOptions,\n  ResourceLimits,\n  Frame,\n  ExceptionInfo,\n  StartOptions,\n  ResumeOptions,\n  ExceptionInput,\n  SnapshotLoadOptions,\n  NameLookupResumeOptions,\n  NameLookupLoadOptions,\n  JsMontyObject,\n}\n\n/**\n * Alias for ResourceLimits (deprecated name).\n */\nexport type JsResourceLimits = ResourceLimits\n\n/**\n * Base class for all Monty interpreter errors.\n *\n * This is the parent class for `MontySyntaxError`, `MontyRuntimeError`, and `MontyTypingError`.\n * Catching `MontyError` will catch any exception raised by Monty.\n */\nexport class MontyError extends Error {\n  protected _typeName: string\n  protected _message: string\n\n  constructor(typeName: string, message: string) {\n    super(message ? `${typeName}: ${message}` : typeName)\n    this.name = 'MontyError'\n    this._typeName = typeName\n    this._message = message\n    // Maintains proper stack trace for where our error was thrown (only available on V8)\n    if (Error.captureStackTrace) {\n      Error.captureStackTrace(this, MontyError)\n    }\n  }\n\n  /**\n   * Returns information about the inner Python exception.\n   */\n  get exception(): ExceptionInfo {\n    return {\n      typeName: this._typeName,\n      message: this._message,\n    }\n  }\n\n  /**\n   * Returns formatted exception string.\n   * @param format - 'type-msg' for 'ExceptionType: message', 'msg' for just the message\n   */\n  display(format: 'type-msg' | 'msg' = 'msg'): string {\n    switch (format) {\n      case 'msg':\n        return this._message\n      case 'type-msg':\n        return this._message ? `${this._typeName}: ${this._message}` : this._typeName\n      default:\n        throw new Error(`Invalid display format: '${format}'. Expected 'type-msg' or 'msg'`)\n    }\n  }\n}\n\n/**\n * Raised when Python code has syntax errors or cannot be parsed by Monty.\n *\n * The inner exception is always a `SyntaxError`. Use `display()` to get\n * formatted error output.\n */\nexport class MontySyntaxError extends MontyError {\n  private _native: NativeMontyException | null\n\n  constructor(messageOrNative: string | NativeMontyException) {\n    if (typeof messageOrNative === 'string') {\n      super('SyntaxError', messageOrNative)\n      this._native = null\n    } else {\n      const exc = messageOrNative.exception\n      super('SyntaxError', exc.message)\n      this._native = messageOrNative\n    }\n    this.name = 'MontySyntaxError'\n    if (Error.captureStackTrace) {\n      Error.captureStackTrace(this, MontySyntaxError)\n    }\n  }\n\n  /**\n   * Returns formatted exception string.\n   * @param format - 'type-msg' for 'SyntaxError: message', 'msg' for just the message\n   */\n  override display(format: 'type-msg' | 'msg' = 'msg'): string {\n    if (this._native && typeof this._native.display === 'function') {\n      return this._native.display(format)\n    }\n    return super.display(format)\n  }\n}\n\n/**\n * Raised when Monty code fails during execution.\n *\n * Provides access to the traceback frames where the error occurred via `traceback()`,\n * and formatted output via `display()`.\n */\nexport class MontyRuntimeError extends MontyError {\n  private _native: NativeMontyException | null\n  private _tracebackString: string | null\n  private _frames: Frame[] | null\n\n  constructor(\n    nativeOrTypeName: NativeMontyException | string,\n    message?: string,\n    tracebackString?: string,\n    frames?: Frame[],\n  ) {\n    if (typeof nativeOrTypeName === 'string') {\n      // Legacy constructor: (typeName, message, tracebackString, frames)\n      super(nativeOrTypeName, message!)\n      this._native = null\n      this._tracebackString = tracebackString ?? null\n      this._frames = frames ?? null\n    } else {\n      // New constructor: (nativeException)\n      const exc = nativeOrTypeName.exception\n      super(exc.typeName, exc.message)\n      this._native = nativeOrTypeName\n      this._tracebackString = null\n      this._frames = null\n    }\n    this.name = 'MontyRuntimeError'\n    if (Error.captureStackTrace) {\n      Error.captureStackTrace(this, MontyRuntimeError)\n    }\n  }\n\n  /**\n   * Returns the Monty traceback as an array of Frame objects.\n   */\n  traceback(): Frame[] {\n    if (this._native) {\n      return this._native.traceback()\n    }\n    return this._frames || []\n  }\n\n  /**\n   * Returns formatted exception string.\n   * @param format - 'traceback' for full traceback, 'type-msg' for 'ExceptionType: message', 'msg' for just the message\n   */\n  display(format: 'traceback' | 'type-msg' | 'msg' = 'traceback'): string {\n    if (this._native && typeof this._native.display === 'function') {\n      return this._native.display(format)\n    }\n    // Fallback for legacy constructor\n    switch (format) {\n      case 'traceback':\n        return this._tracebackString || this.message\n      case 'type-msg':\n        return this._message ? `${this._typeName}: ${this._message}` : this._typeName\n      case 'msg':\n        return this._message\n      default:\n        throw new Error(`Invalid display format: '${format}'. Expected 'traceback', 'type-msg', or 'msg'`)\n    }\n  }\n}\n\nexport type TypingDisplayFormat =\n  | 'full'\n  | 'concise'\n  | 'azure'\n  | 'json'\n  | 'jsonlines'\n  | 'rdjson'\n  | 'pylint'\n  | 'gitlab'\n  | 'github'\n\n/**\n * Raised when type checking finds errors in the code.\n *\n * This exception is raised when static type analysis detects type errors.\n * Use `displayDiagnostics()` to render rich diagnostics in various formats for tooling integration.\n * Use `display()` (inherited) for simple 'type-msg' or 'msg' formats.\n */\nexport class MontyTypingError extends MontyError {\n  private _native: NativeMontyTypingError | null\n\n  constructor(messageOrNative: string | NativeMontyTypingError, nativeError: NativeMontyTypingError | null = null) {\n    if (typeof messageOrNative === 'string') {\n      super('TypeError', messageOrNative)\n      this._native = nativeError\n    } else {\n      const exc = messageOrNative.exception\n      super('TypeError', exc.message)\n      this._native = messageOrNative\n    }\n    this.name = 'MontyTypingError'\n    if (Error.captureStackTrace) {\n      Error.captureStackTrace(this, MontyTypingError)\n    }\n  }\n\n  /**\n   * Renders rich type error diagnostics for tooling integration.\n   *\n   * @param format - Output format (default: 'full')\n   * @param color - Include ANSI color codes (default: false)\n   */\n  displayDiagnostics(format: TypingDisplayFormat = 'full', color: boolean = false): string {\n    if (this._native && typeof this._native.display === 'function') {\n      return this._native.display(format, color)\n    }\n    return this._message\n  }\n}\n\n/**\n * Wrapped Monty class that throws proper Error subclasses.\n */\nexport class Monty {\n  private _native: NativeMonty\n\n  /**\n   * Creates a new Monty interpreter by parsing the given code.\n   *\n   * @param code - Python code to execute\n   * @param options - Configuration options\n   * @throws {MontySyntaxError} If the code has syntax errors\n   * @throws {MontyTypingError} If type checking is enabled and finds errors\n   */\n  constructor(code: string, options?: MontyOptions) {\n    const result = NativeMonty.create(code, options)\n\n    if (result instanceof NativeMontyException) {\n      // Check typeName to distinguish syntax errors from other exceptions\n      if (result.exception.typeName === 'SyntaxError') {\n        throw new MontySyntaxError(result)\n      }\n      throw new MontyRuntimeError(result)\n    }\n    if (result instanceof NativeMontyTypingError) {\n      throw new MontyTypingError(result)\n    }\n\n    this._native = result\n  }\n\n  /**\n   * Performs static type checking on the code.\n   *\n   * @param prefixCode - Optional code to prepend before type checking\n   * @throws {MontyTypingError} If type checking finds errors\n   */\n  typeCheck(prefixCode?: string): void {\n    const result = this._native.typeCheck(prefixCode)\n    if (result instanceof NativeMontyTypingError) {\n      throw new MontyTypingError(result)\n    }\n  }\n\n  /**\n   * Executes the code and returns the result.\n   *\n   * @param options - Execution options (inputs, limits)\n   * @returns The result of the last expression\n   * @throws {MontyRuntimeError} If the code raises an exception\n   */\n  run(options?: RunOptions): JsMontyObject {\n    const result = this._native.run(options)\n    if (result instanceof NativeMontyException) {\n      throw new MontyRuntimeError(result)\n    }\n    return result\n  }\n\n  /**\n   * Starts execution and returns a snapshot (paused at external call or name lookup) or completion.\n   *\n   * @param options - Execution options (inputs, limits)\n   * @returns MontySnapshot if paused at function call, MontyNameLookup if paused at\n   *   name lookup, MontyComplete if done\n   * @throws {MontyRuntimeError} If the code raises an exception\n   */\n  start(options?: StartOptions): MontySnapshot | MontyNameLookup | MontyComplete {\n    const result = this._native.start(options)\n    return wrapStartResult(result)\n  }\n\n  /**\n   * Serializes the Monty instance to a binary format.\n   */\n  dump(): Buffer {\n    return this._native.dump()\n  }\n\n  /**\n   * Deserializes a Monty instance from binary format.\n   */\n  static load(data: Buffer): Monty {\n    const instance = Object.create(Monty.prototype) as Monty\n    instance._native = NativeMonty.load(data)\n    return instance\n  }\n\n  /** Returns the script name. */\n  get scriptName(): string {\n    return this._native.scriptName\n  }\n\n  /** Returns the input variable names. */\n  get inputs(): string[] {\n    return this._native.inputs\n  }\n\n  /** Returns a string representation of the Monty instance. */\n  repr(): string {\n    return this._native.repr()\n  }\n}\n\n/** Options for creating a new MontyRepl instance. */\nexport interface MontyReplOptions {\n  /** Name used in tracebacks and error messages. Default: 'main.py' */\n  scriptName?: string\n  /** Resource limits applied to all snippet executions. */\n  limits?: ResourceLimits\n}\n\n/**\n * Incremental no-replay REPL session.\n *\n * Create with `new MontyRepl()` then call `feed()` to execute snippets\n * incrementally against persistent state.\n */\nexport class MontyRepl {\n  private _native: NativeMontyRepl\n\n  /**\n   * Creates an empty REPL session ready to receive snippets via `feed()`.\n   *\n   * @param options - Optional configuration (scriptName, limits)\n   */\n  constructor(options?: MontyReplOptions) {\n    this._native = new NativeMontyRepl(options)\n  }\n\n  /** Returns the script name for this REPL session. */\n  get scriptName(): string {\n    return this._native.scriptName\n  }\n\n  /**\n   * Executes one incremental snippet.\n   *\n   * @param code - Snippet code to execute\n   * @returns Snippet output\n   * @throws {MontyRuntimeError} If execution raises an exception\n   */\n  feed(code: string): JsMontyObject {\n    const result = this._native.feed(code)\n    if (result instanceof NativeMontyException) {\n      throw new MontyRuntimeError(result)\n    }\n    return result\n  }\n\n  /** Serializes the REPL session to bytes. */\n  dump(): Buffer {\n    return this._native.dump()\n  }\n\n  /** Restores a REPL session from bytes. */\n  static load(data: Buffer): MontyRepl {\n    const native = NativeMontyRepl.load(data)\n    const repl = Object.create(MontyRepl.prototype) as MontyRepl\n    ;(repl as any)._native = native\n    return repl\n  }\n\n  /** Returns a string representation of the REPL session. */\n  repr(): string {\n    return this._native.repr()\n  }\n}\n\n/**\n * Helper to wrap native start/resume results, throwing errors as needed.\n */\nfunction wrapStartResult(\n  result: NativeMontySnapshot | NativeMontyNameLookup | NativeMontyComplete | NativeMontyException,\n): MontySnapshot | MontyNameLookup | MontyComplete {\n  if (result instanceof NativeMontyException) {\n    throw new MontyRuntimeError(result)\n  }\n  // Check MontyNameLookup before MontySnapshot — napi `Either4` may cause\n  // false positives with `instanceof` if checked in the wrong order.\n  if (result instanceof NativeMontyNameLookup) {\n    return new MontyNameLookup(result)\n  }\n  if (result instanceof NativeMontySnapshot) {\n    return new MontySnapshot(result)\n  }\n  if (result instanceof NativeMontyComplete) {\n    return new MontyComplete(result)\n  }\n  throw new Error(`Unexpected result type from native binding: ${result}`)\n}\n\n/**\n * Represents paused execution waiting for an external function call return value.\n *\n * Contains information about the pending external function call and allows\n * resuming execution with the return value or an exception.\n */\nexport class MontySnapshot {\n  private _native: NativeMontySnapshot\n\n  constructor(nativeSnapshot: NativeMontySnapshot) {\n    this._native = nativeSnapshot\n  }\n\n  /** Returns the name of the script being executed. */\n  get scriptName(): string {\n    return this._native.scriptName\n  }\n\n  /** Returns the name of the external function being called. */\n  get functionName(): string {\n    return this._native.functionName\n  }\n\n  /** Returns the positional arguments passed to the external function. */\n  get args(): JsMontyObject[] {\n    return this._native.args\n  }\n\n  /** Returns the keyword arguments passed to the external function as an object. */\n  get kwargs(): Record<string, JsMontyObject> {\n    return this._native.kwargs as Record<string, JsMontyObject>\n  }\n\n  /**\n   * Resumes execution with either a return value or an exception.\n   *\n   * @param options - Object with either `returnValue` or `exception`\n   * @returns MontySnapshot if paused at function call, MontyNameLookup if paused at\n   *   name lookup, MontyComplete if done\n   * @throws {MontyRuntimeError} If the code raises an exception\n   */\n  resume(options: ResumeOptions): MontySnapshot | MontyNameLookup | MontyComplete {\n    const result = this._native.resume(options)\n    return wrapStartResult(result)\n  }\n\n  /**\n   * Serializes the MontySnapshot to a binary format.\n   */\n  dump(): Buffer {\n    return this._native.dump()\n  }\n\n  /**\n   * Deserializes a MontySnapshot from binary format.\n   */\n  static load(data: Buffer, options?: SnapshotLoadOptions): MontySnapshot {\n    const nativeSnapshot = NativeMontySnapshot.load(data, options)\n    return new MontySnapshot(nativeSnapshot)\n  }\n\n  /** Returns a string representation of the MontySnapshot. */\n  repr(): string {\n    return this._native.repr()\n  }\n}\n\n/**\n * Represents paused execution waiting for a name to be resolved.\n *\n * The host should check if the variable name corresponds to a known value\n * (e.g., an external function). Call `resume()` with the value to continue\n * execution, or call `resume()` with no value to raise `NameError`.\n */\nexport class MontyNameLookup {\n  private _native: NativeMontyNameLookup\n\n  constructor(nativeNameLookup: NativeMontyNameLookup) {\n    this._native = nativeNameLookup\n  }\n\n  /** Returns the name of the script being executed. */\n  get scriptName(): string {\n    return this._native.scriptName\n  }\n\n  /** Returns the name of the variable being looked up. */\n  get variableName(): string {\n    return this._native.variableName\n  }\n\n  /**\n   * Resumes execution after resolving the name lookup.\n   *\n   * If `value` is provided, the name resolves to that value and execution continues.\n   * If `value` is omitted/undefined, the VM raises a `NameError`.\n   *\n   * @param options - Optional object with `value` to resolve the name to\n   * @returns MontySnapshot if paused at function call, MontyNameLookup if paused at\n   *   another name lookup, MontyComplete if done\n   * @throws {MontyRuntimeError} If the code raises an exception\n   */\n  resume(options?: NameLookupResumeOptions): MontySnapshot | MontyNameLookup | MontyComplete {\n    const result = this._native.resume(options)\n    return wrapStartResult(result)\n  }\n\n  /**\n   * Serializes the MontyNameLookup to a binary format.\n   */\n  dump(): Buffer {\n    return this._native.dump()\n  }\n\n  /**\n   * Deserializes a MontyNameLookup from binary format.\n   */\n  static load(data: Buffer, options?: NameLookupLoadOptions): MontyNameLookup {\n    const nativeLookup = NativeMontyNameLookup.load(data, options)\n    return new MontyNameLookup(nativeLookup)\n  }\n\n  /** Returns a string representation of the MontyNameLookup. */\n  repr(): string {\n    return this._native.repr()\n  }\n}\n\n/**\n * Represents completed execution with a final output value.\n */\nexport class MontyComplete {\n  private _native: NativeMontyComplete\n\n  constructor(nativeComplete: NativeMontyComplete) {\n    this._native = nativeComplete\n  }\n\n  /** Returns the final output value from the executed code. */\n  get output(): JsMontyObject {\n    return this._native.output\n  }\n\n  /** Returns a string representation of the MontyComplete. */\n  repr(): string {\n    return this._native.repr()\n  }\n}\n\n/**\n * Options for `runMontyAsync`.\n */\nexport interface RunMontyAsyncOptions {\n  /** Input values for the script. */\n  inputs?: Record<string, JsMontyObject>\n  /** External function implementations (sync or async). */\n  externalFunctions?: Record<string, (...args: unknown[]) => unknown>\n  /** Resource limits. */\n  limits?: ResourceLimits\n  /** Callback invoked on each print() call. The first argument is the stream name (always \"stdout\"), the second is the printed text. */\n  printCallback?: (stream: string, text: string) => void\n}\n\n/**\n * Runs a Monty script with async external function support.\n *\n * This function handles both synchronous and asynchronous external functions.\n * When an external function returns a Promise, it will be awaited before\n * resuming execution.\n *\n * @param montyRunner - The Monty runner instance to execute\n * @param options - Execution options\n * @returns The output of the Monty script\n * @throws {MontyRuntimeError} If the code raises an exception\n * @throws {MontySyntaxError} If the code has syntax errors\n *\n * @example\n * const m = new Monty('result = await fetch_data(url)', {\n *   inputs: ['url'],\n * });\n *\n * const result = await runMontyAsync(m, {\n *   inputs: { url: 'https://example.com' },\n *   externalFunctions: {\n *     fetch_data: async (url) => {\n *       const response = await fetch(url);\n *       return response.text();\n *     }\n *   }\n * });\n */\nexport async function runMontyAsync(montyRunner: Monty, options: RunMontyAsyncOptions = {}): Promise<JsMontyObject> {\n  const { inputs, externalFunctions = {}, limits, printCallback } = options\n\n  let progress: MontySnapshot | MontyNameLookup | MontyComplete = montyRunner.start({\n    inputs,\n    limits,\n    printCallback,\n  })\n\n  while (!(progress instanceof MontyComplete)) {\n    if (progress instanceof MontyNameLookup) {\n      // Name lookup — check if the name is a known external function\n      const name = progress.variableName\n      const extFunction = externalFunctions[name]\n      if (extFunction) {\n        // Resolve the name as a function value\n        progress = progress.resume({ value: extFunction })\n      } else {\n        // Unknown name — resume with no value to raise NameError\n        progress = progress.resume()\n      }\n      continue\n    }\n\n    // MontySnapshot — external function call\n    const snapshot = progress\n    const funcName = snapshot.functionName\n    const extFunction = externalFunctions[funcName]\n\n    if (!extFunction) {\n      // Function not found — this shouldn't normally happen since NameLookup\n      // would have raised NameError, but handle it defensively\n      progress = snapshot.resume({\n        exception: {\n          type: 'NameError',\n          message: `name '${funcName}' is not defined`,\n        },\n      })\n      continue\n    }\n\n    try {\n      // Call the external function\n      let result = extFunction(...snapshot.args, snapshot.kwargs)\n\n      // If the result is a Promise, await it\n      if (result && typeof (result as Promise<unknown>).then === 'function') {\n        result = await result\n      }\n\n      // Resume with the return value\n      progress = snapshot.resume({ returnValue: result })\n    } catch (error) {\n      // External function threw an exception - convert to Monty exception\n      const err = error as Error\n      const excType = err.name || 'RuntimeError'\n      const excMessage = err.message || String(error)\n      progress = snapshot.resume({\n        exception: {\n          type: excType,\n          message: excMessage,\n        },\n      })\n    }\n  }\n\n  return progress.output\n}\n"
  },
  {
    "path": "crates/monty-python/Cargo.toml",
    "content": "[package]\nname = \"pydantic-monty\"\ndescription = \"Python bindings for the Monty sandboxed Python interpreter\"\nreadme = \"README.md\"\nversion = { workspace = true }\nrust-version = { workspace = true }\nedition = { workspace = true }\nauthors = { workspace = true }\nlicense = { workspace = true }\nkeywords = { workspace = true }\ncategories = { workspace = true }\nhomepage = { workspace = true }\nrepository = { workspace = true }\n\n[lib]\nname = \"_monty\"\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nmonty = { path = \"../monty\" }\nmonty_type_checking = { path = \"../monty-type-checking\" }\npyo3 = { version = \"0.28\", features = [\"indexmap\", \"generate-import-lib\", \"num-bigint\"] }\nnum-bigint = { workspace = true }\nindexmap = { workspace = true }\nserde = { workspace = true }\npostcard = { workspace = true }\nsend_wrapper = \"0.6.0\"\nsha2 = { workspace = true }\n\n[build-dependencies]\npyo3-build-config = { version = \"0.28\", features = [\"resolve-config\"] }\n\n[features]\n# make extensions visible to cargo vendor\nextension-module = [\"pyo3/generate-import-lib\"]\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/monty-python/README.md",
    "content": "# pydantic-monty\n\nPython bindings for the Monty sandboxed Python interpreter.\n\n## Installation\n\n```bash\npip install pydantic-monty\n```\n\n## Usage\n\n### Basic Expression Evaluation\n\n```python\nimport pydantic_monty\n\n# Simple code with no inputs\nm = pydantic_monty.Monty('1 + 2')\nprint(m.run())\n#> 3\n```\n\n### Using Input Variables\n\n```python\nimport pydantic_monty\n\n# Create with code that uses input variables\nm = pydantic_monty.Monty('x * y', inputs=['x', 'y'])\n\n# Run multiple times with different inputs\nprint(m.run(inputs={'x': 2, 'y': 3}))\n#> 6\nprint(m.run(inputs={'x': 10, 'y': 5}))\n#> 50\n```\n\n### Resource Limits\n\n```python\nimport pydantic_monty\n\nm = pydantic_monty.Monty('x + y', inputs=['x', 'y'])\n\n# With resource limits\nlimits = pydantic_monty.ResourceLimits(max_duration_secs=1.0)\nresult = m.run(inputs={'x': 1, 'y': 2}, limits=limits)\nassert result == 3\n```\n\n### External Functions\n\n```python\nimport pydantic_monty\n\n# Code that calls an external function\nm = pydantic_monty.Monty('double(x)', inputs=['x'])\n\n# Provide the external function implementation at runtime\nresult = m.run(inputs={'x': 5}, external_functions={'double': lambda x: x * 2})\nprint(result)\n#> 10\n```\n\n### Iterative Execution with External Functions\n\nUse `start()` and `resume()` to handle external function calls iteratively,\ngiving you control over each call:\n\n```python\nimport pydantic_monty\n\ncode = \"\"\"\ndata = fetch(url)\nlen(data)\n\"\"\"\n\nm = pydantic_monty.Monty(code, inputs=['url'])\n\n# Start execution - pauses when fetch() is called\nresult = m.start(inputs={'url': 'https://example.com'})\n\nprint(type(result))\n#> <class 'pydantic_monty.FunctionSnapshot'>\nprint(result.function_name)  # fetch\n#> fetch\nprint(result.args)\n#> ('https://example.com',)\n\n# Perform the actual fetch, then resume with the result\nresult = result.resume(return_value='hello world')\n\nprint(type(result))\n#> <class 'pydantic_monty.MontyComplete'>\nprint(result.output)\n#> 11\n```\n\n### Serialization\n\nBoth `Monty` and `FunctionSnapshot` can be serialized to bytes and restored later.\nThis allows caching parsed code or suspending execution across process boundaries:\n\n```python\nimport pydantic_monty\n\n# Serialize parsed code to avoid re-parsing\nm = pydantic_monty.Monty('x + 1', inputs=['x'])\ndata = m.dump()\n\n# Later, restore and run\nm2 = pydantic_monty.Monty.load(data)\nprint(m2.run(inputs={'x': 41}))\n#> 42\n```\n\nExecution state can also be serialized mid-flight:\n\n```python\nimport pydantic_monty\n\nm = pydantic_monty.Monty('fetch(url)', inputs=['url'])\nprogress = m.start(inputs={'url': 'https://example.com'})\n\n# Serialize the execution state\nstate = progress.dump()\n\n# Later, restore and resume (e.g., in a different process)\nprogress2 = pydantic_monty.load_snapshot(state)\nresult = progress2.resume(return_value='response data')\nprint(result.output)\n#> response data\n```\n"
  },
  {
    "path": "crates/monty-python/build.rs",
    "content": "fn main() {\n    // see https://pyo3.rs/main/building-and-distribution/multiple-python-versions.html\n    pyo3_build_config::use_pyo3_cfgs();\n}\n"
  },
  {
    "path": "crates/monty-python/example.py",
    "content": "\"\"\"Example usage of the Monty Python bindings.\"\"\"\n\nimport pydantic_monty\n\n# Basic execution - simple expression\nm = pydantic_monty.Monty('1 + 2 * 3')\nprint(f'Basic: {m.run()!r}')  # 7\n\n# Using input variables\nm = pydantic_monty.Monty('x + y', inputs=['x', 'y'])\nprint(f'Inputs: {m.run(inputs={\"x\": 10, \"y\": 20})}')  # 30\n\n# Reusing the same parsed code with different values\nprint(f'Reuse: {m.run(inputs={\"x\": 100, \"y\": 200})}')  # 300\n\n# With resource limits\nlimits = pydantic_monty.ResourceLimits(max_duration_secs=5.0, max_memory=1024 * 1024)\nm = pydantic_monty.Monty('x * y * z', inputs=['x', 'y', 'z'])\nprint(f'With limits: {m.run(inputs={\"x\": 2, \"y\": 3, \"z\": 4}, limits=limits)}')  # 24\n\n# External function callbacks\nm = pydantic_monty.Monty('fetch(\"https://example.com\")')\n\n\ndef fetch(url: str) -> str:\n    return f'Fetched: {url}'\n\n\nprint(f'External: {m.run(external_functions={\"fetch\": fetch})}')\n\n# Print output is forwarded to Python stdout\nm = pydantic_monty.Monty('print(\"Hello from Monty!\")')\nm.run()\n\n# Exception handling\nm = pydantic_monty.Monty('1 / 0')\ntry:\n    m.run()\nexcept ZeroDivisionError as e:\n    print(f'Caught: {type(e).__name__}')\n"
  },
  {
    "path": "crates/monty-python/exercise.py",
    "content": "\"\"\"\nExercise script for PGO data collection.\n\nRuns all test cases through Monty with type checking enabled,\nexercising the full interpreter pipeline for profiling.\n\"\"\"\n\nimport time\nfrom pathlib import Path\n\nimport pydantic_monty\n\n\ndef main():\n    test_cases = Path(__file__).parent.parent / 'monty' / 'test_cases'\n    run, run_success, type_errors = 0, 0, 0\n    start = time.perf_counter()\n\n    for py_file in test_cases.glob('*.py'):\n        code = py_file.read_text(encoding='utf-8')\n\n        # Exercise parsing and type checking\n        try:\n            try:\n                m = pydantic_monty.Monty(code, type_check=True)\n            except pydantic_monty.MontyTypingError:\n                # Many test cases have type errors\n                m = pydantic_monty.Monty(code)\n                type_errors += 1\n\n            # Exercise execution\n            run += 1\n            m.run(print_callback=lambda _, __: None)\n            run_success += 1\n        except pydantic_monty.MontyError:\n            # ignore syntax errors or errors while running the code\n            pass\n        except Exception as e:\n            raise RuntimeError(f'Error running {py_file.name}: {e}') from e\n\n    t = time.perf_counter() - start\n    print(f'Executed {run} test cases in {t:.2f} seconds, {run_success} succeeded, {type_errors} had type errors')\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "crates/monty-python/pyproject.toml",
    "content": "[build-system]\nrequires = [\"maturin>=1.9.4,<2.0\"]\nbuild-backend = \"maturin\"\n\n[project]\n# the module is named `pydantic_monty`\nname = \"pydantic-monty\"\ndescription = \"Python bindings for the Monty sandboxed Python interpreter\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3 :: Only\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Intended Audience :: Developers\",\n    \"Intended Audience :: Information Technology\",\n    \"Intended Audience :: System Administrators\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: Unix\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Environment :: MacOS X\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Topic :: Internet\",\n    \"Programming Language :: Python :: Implementation\",\n]\ndynamic = [\"license\", \"version\"]\n\n[project.urls]\nHomepage = \"https://github.com/pydantic/monty\"\nSource = \"https://github.com/pydantic/monty\"\n\n[tool.maturin]\npython-source = \"python\"\nmodule-name = \"pydantic_monty._monty\"\nfeatures = [\"pyo3/extension-module\"]\n\n[dependency-groups]\ndev = [\n    \"anyio>=4.0\",\n    \"black>=25.12.0\",\n    \"dirty-equals>=0.11\",\n    \"inline-snapshot>=0.31.1\",\n    \"pytest>=9.0.2\",\n    \"pytest-examples>=0.0.14\",\n    \"pytest-pretty>=1.3.0\",\n]\n\n[tool.pytest.ini_options]\nanyio_mode = \"auto\"\nxfail_strict = true\nfilterwarnings = [\"error\"]\n"
  },
  {
    "path": "crates/monty-python/python/pydantic_monty/__init__.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar, cast\n\nif TYPE_CHECKING:\n    from collections.abc import Awaitable\n    from types import EllipsisType\n\nfrom ._monty import (\n    Frame,\n    FunctionSnapshot,\n    FutureSnapshot,\n    Monty,\n    MontyComplete,\n    MontyError,\n    MontyRepl,\n    MontyRuntimeError,\n    MontySyntaxError,\n    MontyTypingError,\n    NameLookupSnapshot,\n    __version__,\n    load_repl_snapshot,\n    load_snapshot,\n)\nfrom .os_access import AbstractFile, AbstractOS, CallbackFile, MemoryFile, OSAccess, OsFunction, StatResult\n\n__all__ = (\n    # this file\n    'run_monty_async',\n    'run_repl_async',\n    'ExternalResult',\n    'ResourceLimits',\n    # _monty\n    '__version__',\n    'Monty',\n    'MontyRepl',\n    'MontyComplete',\n    'FunctionSnapshot',\n    'NameLookupSnapshot',\n    'FutureSnapshot',\n    'MontyError',\n    'MontySyntaxError',\n    'MontyRuntimeError',\n    'MontyTypingError',\n    'Frame',\n    'load_snapshot',\n    'load_repl_snapshot',\n    # os_access\n    'StatResult',\n    'OsFunction',\n    'AbstractOS',\n    'AbstractFile',\n    'MemoryFile',\n    'CallbackFile',\n    'OSAccess',\n)\nT = TypeVar('T')\n\n\nasync def run_monty_async(\n    monty_runner: Monty,\n    *,\n    inputs: dict[str, Any] | None = None,\n    external_functions: dict[str, Callable[..., Any]] | None = None,\n    limits: ResourceLimits | None = None,\n    print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n    os: AbstractOS | None = None,\n) -> Any:\n    \"\"\"Run a Monty script with async external functions and optional OS access.\n\n    This function provides a convenient way to run Monty code that uses both async\n    external functions and filesystem operations via OSAccess.\n\n    Args:\n        monty_runner: The Monty runner to use.\n        external_functions: A dictionary of external functions to use, can be sync or async.\n        inputs: A dictionary of inputs to use.\n        limits: The resource limits to use.\n        print_callback: A callback to use for printing.\n        os: Optional OS access handler for filesystem operations (e.g., OSAccess instance).\n\n    Returns:\n        The output of the Monty script.\n    \"\"\"\n    from functools import partial\n\n    progress = await _run_in_pool(\n        partial(monty_runner.start, inputs=inputs, limits=limits, print_callback=print_callback)\n    )\n    return await _dispatch_loop(progress, external_functions or {}, os)\n\n\nasync def run_repl_async(\n    repl: MontyRepl,\n    code: str,\n    *,\n    inputs: dict[str, Any] | None = None,\n    external_functions: dict[str, Callable[..., Any]] | None = None,\n    print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n    os: AbstractOS | None = None,\n) -> Any:\n    \"\"\"Feed a snippet to a REPL session with async external function support.\n\n    This is the REPL equivalent of `run_monty_async`. It calls `feed_start()` on\n    the REPL and drives the snapshot/resume loop, dispatching external function\n    calls (sync or async), OS calls, dataclass method calls, and future resolution.\n\n    Args:\n        repl: The REPL session to feed the snippet to.\n        code: The Python code snippet to execute.\n        external_functions: A dictionary of external functions to use, can be sync or async.\n        inputs: A dictionary of inputs to use.\n        print_callback: A callback to use for printing.\n        os: Optional OS access handler for filesystem operations (e.g., OSAccess instance).\n\n    Returns:\n        The output of the snippet.\n    \"\"\"\n    from functools import partial\n\n    progress = await _run_in_pool(partial(repl.feed_start, code, inputs=inputs, print_callback=print_callback))\n    return await _dispatch_loop(progress, external_functions or {}, os)\n\n\nasync def _run_in_pool(func: Callable[[], T]) -> T:\n    \"\"\"Run a function in a thread pool executor, releasing the GIL.\"\"\"\n    import asyncio\n    from concurrent.futures import ThreadPoolExecutor\n\n    loop = asyncio.get_running_loop()\n    with ThreadPoolExecutor() as pool:\n        return await loop.run_in_executor(pool, func)\n\n\nasync def _dispatch_loop(\n    progress: FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete,\n    external_functions: dict[str, Callable[..., Any]],\n    os: AbstractOS | None,\n) -> Any:\n    \"\"\"Drive the snapshot/resume loop for both Monty and MontyRepl.\n\n    Handles external function calls (sync and async), OS calls, dataclass method\n    calls, name lookups, and future resolution.\n    \"\"\"\n    import asyncio\n    import inspect\n    from functools import partial\n\n    tasks: dict[int, asyncio.Task[tuple[int, ExternalResult]]] = {}\n\n    try:\n        while True:\n            if isinstance(progress, MontyComplete):\n                return progress.output\n            elif isinstance(progress, FunctionSnapshot):\n                # Handle OS function calls (e.g., Path.read_text, Path.exists)\n                if progress.is_os_function:\n                    # When is_os_function is True, function_name is always an OsFunction\n                    os_func_name = cast(OsFunction, progress.function_name)\n                    if os is None:\n                        e = NotImplementedError(\n                            f'OS function {progress.function_name} called but no os handler provided'\n                        )\n                        progress = await _run_in_pool(partial(progress.resume, exception=e))\n                    else:\n                        try:\n                            result = os(os_func_name, progress.args, progress.kwargs)\n                        except Exception as exc:\n                            progress = await _run_in_pool(partial(progress.resume, exception=exc))\n                        else:\n                            progress = await _run_in_pool(partial(progress.resume, return_value=result))\n                # Handle dataclass method calls (first arg is the instance)\n                elif progress.is_method_call:\n                    self_obj = progress.args[0]\n                    method = getattr(self_obj, progress.function_name)\n                    remaining_args = progress.args[1:]\n                    try:\n                        result = method(*remaining_args, **progress.kwargs)\n                    except Exception as exc:\n                        progress = await _run_in_pool(partial(progress.resume, exception=exc))\n                    else:\n                        if inspect.iscoroutine(result):\n                            call_id = progress.call_id\n                            tasks[call_id] = asyncio.create_task(_run_external_function(call_id, result))\n                            progress = await _run_in_pool(partial(progress.resume, future=...))\n                        else:\n                            progress = await _run_in_pool(partial(progress.resume, return_value=result))\n                # Handle external function calls\n                elif ext_function := external_functions.get(progress.function_name):\n                    try:\n                        result = ext_function(*progress.args, **progress.kwargs)\n                    except Exception as exc:\n                        progress = await _run_in_pool(partial(progress.resume, exception=exc))\n                    else:\n                        if inspect.iscoroutine(result):\n                            call_id = progress.call_id\n                            tasks[call_id] = asyncio.create_task(_run_external_function(call_id, result))\n                            progress = await _run_in_pool(partial(progress.resume, future=...))\n                        else:\n                            progress = await _run_in_pool(partial(progress.resume, return_value=result))\n                else:\n                    e = LookupError(f\"Unable to find '{progress.function_name}' in external functions dict\")\n                    progress = await _run_in_pool(partial(progress.resume, exception=e))\n            elif isinstance(progress, NameLookupSnapshot):\n                ext_function = external_functions.get(progress.variable_name)\n                if ext_function is not None:\n                    progress = await _run_in_pool(partial(progress.resume, value=ext_function))\n                else:\n                    progress = await _run_in_pool(progress.resume)\n            else:\n                assert isinstance(progress, FutureSnapshot), f'Unexpected progress type {progress!r}'\n\n                current_tasks: list[asyncio.Task[tuple[int, ExternalResult]]] = []\n                for call_id in progress.pending_call_ids:\n                    if task := tasks.get(call_id):\n                        current_tasks.append(task)\n\n                done, _ = await asyncio.wait(current_tasks, return_when=asyncio.FIRST_COMPLETED)\n\n                results: dict[int, ExternalResult] = {}\n                for task in done:\n                    call_id, result = task.result()\n                    results[call_id] = result\n                    tasks.pop(call_id)\n\n                progress = await _run_in_pool(partial(progress.resume, results))\n\n    finally:\n        for task in tasks.values():\n            task.cancel()\n        try:\n            await asyncio.gather(*tasks.values())\n        except asyncio.CancelledError:\n            pass\n\n\nasync def _run_external_function(call_id: int, coro: Awaitable[Any]) -> tuple[int, ExternalResult]:\n    try:\n        result = await coro\n    except Exception as e:\n        return call_id, ExternalException(exception=e)\n    else:\n        return call_id, ExternalReturnValue(return_value=result)\n\n\nclass ResourceLimits(TypedDict, total=False):\n    \"\"\"\n    Configuration for resource limits during code execution.\n\n    All limits are optional. Omit a key to disable that limit.\n    \"\"\"\n\n    max_allocations: int\n    \"\"\"Maximum number of heap allocations allowed.\"\"\"\n\n    max_duration_secs: float\n    \"\"\"Maximum execution time in seconds.\"\"\"\n\n    max_memory: int\n    \"\"\"Maximum heap memory in bytes.\"\"\"\n\n    gc_interval: int\n    \"\"\"Run garbage collection every N allocations.\"\"\"\n\n    max_recursion_depth: int\n    \"\"\"Maximum function call stack depth (default: 1000).\"\"\"\n\n\nclass ExternalReturnValue(TypedDict):\n    return_value: Any\n\n\nclass ExternalException(TypedDict):\n    exception: Exception\n\n\nclass ExternalFuture(TypedDict):\n    future: EllipsisType\n\n\nExternalResult = ExternalReturnValue | ExternalException | ExternalFuture\n"
  },
  {
    "path": "crates/monty-python/python/pydantic_monty/_monty.pyi",
    "content": "from types import EllipsisType\nfrom typing import Any, Callable, Literal, final, overload\n\nfrom typing_extensions import Self\n\nfrom . import ExternalResult, ResourceLimits\nfrom .os_access import OsFunction\n\n__all__ = [\n    '__version__',\n    'Monty',\n    'MontyRepl',\n    'MontyComplete',\n    'FunctionSnapshot',\n    'NameLookupSnapshot',\n    'FutureSnapshot',\n    'MontyError',\n    'MontySyntaxError',\n    'MontyRuntimeError',\n    'MontyTypingError',\n    'Frame',\n    'load_snapshot',\n    'load_repl_snapshot',\n]\n__version__: str\n\n@final\nclass Monty:\n    \"\"\"\n    A sandboxed Python interpreter instance.\n\n    Parses and compiles Python code on initialization, then can be run\n    multiple times with different input values. This separates the parsing\n    cost from execution, making repeated runs more efficient.\n    \"\"\"\n\n    def __new__(\n        cls,\n        code: str,\n        *,\n        script_name: str = 'main.py',\n        inputs: list[str] | None = None,\n        type_check: bool = False,\n        type_check_stubs: str | None = None,\n        dataclass_registry: list[type] | None = None,\n    ) -> Self:\n        \"\"\"\n        Create a new Monty interpreter by parsing the given code.\n\n        Arguments:\n            code: Python code to execute\n            script_name: Name used in tracebacks and error messages\n            inputs: List of input variable names available in the code\n            type_check: Whether to perform type checking on the code (default: True)\n            type_check_stubs: Optional code to prepend before type checking,\n                e.g. with input variable declarations or external function signatures\n            dataclass_registry: Optional list of dataclass types to register for proper\n                isinstance() support on output, see `register_dataclass()` above.\n\n        Raises:\n            MontySyntaxError: If the code cannot be parsed\n            MontyTypingError: If type_check is True and type errors are found\n        \"\"\"\n\n    def type_check(self, prefix_code: str | None = None) -> None:\n        \"\"\"\n        Perform static type checking on the code.\n\n        Analyzes the code for type errors without executing it. This uses\n        a subset of Python's type system supported by Monty.\n\n        Arguments:\n            prefix_code: Optional code to prepend before type checking,\n                e.g. with input variable declarations or external function signatures.\n\n        Raises:\n            MontyTypingError: If type errors are found. Use `.display(format, color)`\n                on the exception to render the diagnostics in different formats.\n            RuntimeError: If the type checking infrastructure fails internally.\n        \"\"\"\n\n    def run(\n        self,\n        *,\n        inputs: dict[str, Any] | None = None,\n        limits: ResourceLimits | None = None,\n        external_functions: dict[str, Callable[..., Any]] | None = None,\n        print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n        os: Callable[[OsFunction, tuple[Any, ...]], Any] | None = None,\n    ) -> Any:\n        \"\"\"\n        Execute the code and return the result.\n\n        The GIL is released allowing parallel execution.\n\n        Arguments:\n            inputs: Dict of input variable values (must match names from __init__)\n            limits: Optional resource limits configuration\n            external_functions: Dict of external function callbacks\n            print_callback: Optional callback for print output\n            os: Optional callback for OS calls.\n                Called with (function_name, args) where function_name is like 'Path.exists'\n                and args is a tuple of arguments. Must return the appropriate value for the\n                OS function (e.g., bool for exists(), stat_result for stat()).\n\n        Returns:\n            The result of the last expression in the code\n\n        Raises:\n            MontyRuntimeError: If the code raises an exception during execution\n        \"\"\"\n\n    def start(\n        self,\n        *,\n        inputs: dict[str, Any] | None = None,\n        limits: ResourceLimits | None = None,\n        print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n    ) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"\n        Start the code execution and return a progress object, or completion.\n\n        This allows you to iteratively run code and parse/resume whenever an external function is called.\n\n        The GIL is released allowing parallel execution.\n\n        Arguments:\n            inputs: Dict of input variable values (must match names from __init__)\n            limits: Optional resource limits configuration\n            print_callback: Optional callback for print output\n\n        Returns:\n            FunctionSnapshot if an external function call is pending,\n            NameLookupSnapshot if more futures need to be resolved,\n            FutureSnapshot if futures need to be resolved,\n            MontyComplete if execution finished without external calls.\n\n        Raises:\n            MontyRuntimeError: If the code raises an exception during execution\n        \"\"\"\n\n    def dump(self) -> bytes:\n        \"\"\"\n        Serialize the Monty instance to a binary format.\n\n        The serialized data can be stored and later restored with `Monty.load()`.\n        This allows caching parsed code to avoid re-parsing on subsequent runs.\n\n        Returns:\n            Bytes containing the serialized Monty instance.\n\n        Raises:\n            ValueError: If serialization fails.\n        \"\"\"\n\n    @staticmethod\n    def load(\n        data: bytes,\n        *,\n        dataclass_registry: list[type] | None = None,\n    ) -> Monty:\n        \"\"\"\n        Deserialize a Monty instance from binary format.\n\n        Arguments:\n            data: The serialized Monty data from `dump()`\n            dataclass_registry: Optional list of dataclass types to register for proper\n                isinstance() support on output, see `register_dataclass()` above.\n\n        Returns:\n            A new Monty instance.\n\n        Raises:\n            ValueError: If deserialization fails.\n        \"\"\"\n\n    def register_dataclass(self, cls: type) -> None:\n        \"\"\"\n        Register a dataclass type for proper isinstance() support on output.\n\n        When a dataclass passes through Monty and is returned, it normally becomes\n        an `UnknownDataclass`. By registering the original type, we can use it to\n        instantiate a real instance of that dataclass.\n\n        Arguments:\n            cls: The dataclass type to register.\n\n        Raises:\n            TypeError: If the argument is not a dataclass type.\n        \"\"\"\n\n    def __repr__(self) -> str: ...\n\n@final\nclass MontyRepl:\n    \"\"\"\n    Incremental no-replay REPL session.\n\n    Create with `MontyRepl()` then call `feed_run()` to execute snippets\n    incrementally against persistent heap and namespace state.\n    \"\"\"\n\n    def __new__(\n        cls,\n        *,\n        script_name: str = 'main.py',\n        limits: ResourceLimits | None = None,\n        dataclass_registry: list[type] | None = None,\n    ) -> Self:\n        \"\"\"\n        Create an empty REPL session ready to receive snippets via `feed_run()`.\n\n        No code is parsed or executed at construction time.\n        \"\"\"\n\n    @property\n    def script_name(self) -> str:\n        \"\"\"The name of the script being executed.\"\"\"\n\n    def register_dataclass(self, cls: type) -> None:\n        \"\"\"\n        Register a dataclass type for proper isinstance() support on output.\n        \"\"\"\n\n    def feed_run(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        external_functions: dict[str, Callable[..., Any]] | None = None,\n        print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n        os: Callable[[str, tuple[Any, ...], dict[str, Any]], Any] | None = None,\n    ) -> Any:\n        \"\"\"\n        Execute one incremental snippet and return its output.\n\n        When `inputs` is provided, the key-value pairs are injected into\n        the REPL namespace before executing the snippet.\n\n        When `external_functions` is provided, external function calls and\n        name lookups are dispatched to the provided callables — matching the\n        behavior of `Monty.run(external_functions=...)`.\n        \"\"\"\n\n    def feed_start(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n    ) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"\n        Start executing an incremental snippet, yielding snapshots for external calls.\n\n        Unlike `feed_run()`, which handles external function dispatch internally,\n        `feed_start()` returns a snapshot object whenever the code needs an external\n        function call, OS call, name lookup, or future resolution. The caller provides\n        the result via `snapshot.resume(...)`, which returns the next snapshot or\n        `MontyComplete`.\n\n        This enables the same iterative start/resume pattern used by `Monty.start()`,\n        including support for async external functions via `FutureSnapshot`.\n\n        On completion or error, the REPL state is automatically restored.\n        \"\"\"\n\n    def dump(self) -> bytes:\n        \"\"\"Serialize the REPL session to bytes.\"\"\"\n\n    @staticmethod\n    def load(\n        data: bytes,\n        *,\n        dataclass_registry: list[type] | None = None,\n    ) -> MontyRepl:\n        \"\"\"Restore a REPL session from bytes.\"\"\"\n\n@final\nclass FunctionSnapshot:\n    \"\"\"\n    Represents a paused execution waiting for an external function call return value.\n\n    Contains information about the pending external function call and allows\n    resuming execution with the return value.\n    \"\"\"\n\n    @property\n    def script_name(self) -> str:\n        \"\"\"The name of the script being executed.\"\"\"\n\n    @property\n    def is_os_function(self) -> bool:\n        \"\"\"Whether this snapshot is for an OS function call (e.g., Path.stat).\"\"\"\n\n    @property\n    def is_method_call(self) -> bool:\n        \"\"\"Whether this snapshot is for a dataclass method call (first arg is `self`).\"\"\"\n\n    @property\n    def function_name(self) -> str | OsFunction:\n        \"\"\"The name of the function being called (external function or OS function like 'Path.stat').\n\n        Will be a `OsFunction` if `is_os_function` is `True`.\n        \"\"\"\n\n    @property\n    def args(self) -> tuple[Any, ...]:\n        \"\"\"The positional arguments passed to the external function.\"\"\"\n\n    @property\n    def kwargs(self) -> dict[str, Any]:\n        \"\"\"The keyword arguments passed to the external function.\"\"\"\n\n    @property\n    def call_id(self) -> int:\n        \"\"\"The unique identifier for this external function call.\"\"\"\n\n    @overload\n    def resume(self, *, return_value: Any) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"Resume execution with a return value from the external function.\n\n        `resume` may only be called once on each FunctionSnapshot instance.\n\n        The GIL is released allowing parallel execution.\n\n        Arguments:\n            return_value: The value to return from the external function call.\n            exception: An exception to raise in the Monty interpreter.\n            future: A future to await in the Monty interpreter.\n\n        Returns:\n            FunctionSnapshot if another external function call is pending,\n            FutureSnapshot if another name lookup is pending,\n            FutureSnapshot if futures need to be resolved,\n            MontyComplete if execution finished.\n\n        Raises:\n            TypeError: If both arguments are provided.\n            RuntimeError: If execution has already completed.\n            MontyRuntimeError: If the code raises an exception during execution\n        \"\"\"\n\n    @overload\n    def resume(\n        self, *, exception: BaseException\n    ) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"Resume execution by raising the exception in the Monty interpreter.\n\n        See docstring for the first overload for more information.\n        \"\"\"\n\n    @overload\n    def resume(self, *, future: EllipsisType) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"Resume execution by returning a pending future.\n\n        No result is provided, we simply resume execution stating that a future is pending.\n\n        See docstring for the first overload for more information.\n        \"\"\"\n\n    def dump(self) -> bytes:\n        \"\"\"\n        Serialize the FunctionSnapshot instance to a binary format.\n\n        The serialized data can be restored with `load_snapshot()` or `load_repl_snapshot()`.\n        This allows suspending execution and resuming later, potentially in a different process.\n\n        Note: The `print_callback` is not serialized and must be re-provided via\n        `set_print_callback()` after loading if print output is needed.\n\n        Returns:\n            Bytes containing the serialized FunctionSnapshot instance.\n\n        Raises:\n            ValueError: If serialization fails.\n            RuntimeError: If the progress has already been resumed.\n        \"\"\"\n\n    def __repr__(self) -> str: ...\n\n@final\nclass NameLookupSnapshot:\n    \"\"\"\n    Represents a paused execution waiting for multiple futures to be resolved.\n\n    Contains information about the pending futures and allows resuming execution\n    with the results.\n    \"\"\"\n\n    @property\n    def script_name(self) -> str:\n        \"\"\"The name of the script being executed.\"\"\"\n\n    @property\n    def variable_name(self) -> str:\n        \"\"\"The name of the variable being looked up.\"\"\"\n\n    def resume(\n        self,\n        *,\n        value: Any | None = None,\n    ) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"Resume execution with result the value from a name lookup, if any.\n\n        If no `value` is passed, a `NameError` is raised.\n\n        `resume` may only be called once on each NameLookupSnapshot instance.\n\n        The GIL is released allowing parallel execution.\n\n        Arguments:\n            value: The value from the name lookup, if any.\n\n        Returns:\n            FunctionSnapshot if an external function call is pending,\n            NameLookupSnapshot if more futures need to be resolved,\n            FutureSnapshot if another name lookup is pending,\n            MontyComplete if execution finished.\n\n        Raises:\n            TypeError: If result dict has invalid keys.\n            RuntimeError: If execution has already completed.\n            MontyRuntimeError: If the code raises an exception during execution\n        \"\"\"\n\n    def dump(self) -> bytes:\n        \"\"\"\n        Serialize the NameLookupSnapshot instance to a binary format.\n\n        The serialized data can be restored with `load_snapshot()` or `load_repl_snapshot()`.\n        This allows suspending execution and resuming later, potentially in a different process.\n\n        Note: The `print_callback` is not serialized and must be re-provided via\n        `set_print_callback()` after loading if print output is needed.\n\n        Returns:\n            Bytes containing the serialized NameLookupSnapshot instance.\n\n        Raises:\n            ValueError: If serialization fails.\n            RuntimeError: If the progress has already been resumed.\n        \"\"\"\n\n    def __repr__(self) -> str: ...\n\n@final\nclass FutureSnapshot:\n    \"\"\"\n    Represents a paused execution waiting for multiple futures to be resolved.\n\n    Contains information about the pending futures and allows resuming execution\n    with the results.\n    \"\"\"\n\n    @property\n    def script_name(self) -> str:\n        \"\"\"The name of the script being executed.\"\"\"\n\n    @property\n    def pending_call_ids(self) -> list[int]:\n        \"\"\"The call IDs of the pending futures.\n\n        Raises an error if the snapshot has already been resumed.\n        \"\"\"\n\n    def resume(\n        self,\n        results: dict[int, ExternalResult],\n    ) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot | MontyComplete:\n        \"\"\"Resume execution with results for one or more futures.\n\n        `resume` may only be called once on each FutureSnapshot instance.\n\n        The GIL is released allowing parallel execution.\n\n        Arguments:\n            results: Dict mapping call_id to result dict. Each result dict must have\n                either 'return_value' or 'exception' key (not both).\n\n        Returns:\n            FunctionSnapshot if an external function call is pending,\n            NameLookupSnapshot if more futures need to be resolved,\n            FutureSnapshot if more futures need to be resolved,\n            MontyComplete if execution finished.\n\n        Raises:\n            TypeError: If result dict has invalid keys.\n            RuntimeError: If execution has already completed.\n            MontyRuntimeError: If the code raises an exception during execution\n        \"\"\"\n\n    def dump(self) -> bytes:\n        \"\"\"\n        Serialize the FutureSnapshot instance to a binary format.\n\n        The serialized data can be restored with `load_snapshot()` or `load_repl_snapshot()`.\n        This allows suspending execution and resuming later, potentially in a different process.\n\n        Note: The `print_callback` is not serialized and must be re-provided via\n        `set_print_callback()` after loading if print output is needed.\n\n        Returns:\n            Bytes containing the serialized FutureSnapshot instance.\n\n        Raises:\n            ValueError: If serialization fails.\n            RuntimeError: If the progress has already been resumed.\n        \"\"\"\n\n    def __repr__(self) -> str: ...\n\n@final\nclass MontyComplete:\n    \"\"\"The result of a completed code execution.\"\"\"\n\n    @property\n    def output(self) -> Any:\n        \"\"\"The final output value from the executed code.\"\"\"\n\n    def __repr__(self) -> str: ...\n\nclass MontyError(Exception):\n    \"\"\"Base exception for all Monty interpreter errors.\n\n    Catching `MontyError` will catch syntax, runtime, and typing errors from Monty.\n    This exception is raised internally by Monty and cannot be constructed directly.\n    \"\"\"\n\n    def exception(self) -> BaseException:\n        \"\"\"Returns the inner exception as a Python exception object.\"\"\"\n\n    def __str__(self) -> str:\n        \"\"\"Returns the exception message.\"\"\"\n\n@final\nclass MontySyntaxError(MontyError):\n    \"\"\"Raised when Python code has syntax errors or cannot be parsed by Monty.\n\n    Inherits exception(), __str__() from MontyError.\n    \"\"\"\n\n    def display(self, format: Literal['type-msg', 'msg'] = 'msg') -> str:\n        \"\"\"Returns formatted exception string.\n\n        Args:\n            format: 'type-msg' - 'ExceptionType: message' format\n                  'msg' - just the message\n        \"\"\"\n\n@final\nclass MontyTypingError(MontyError):\n    \"\"\"Raised when type checking finds errors in the code.\n\n    This exception is raised when static type analysis detects type errors\n    before execution. Use `.display(format, color)` to render the diagnostics\n    in different formats.\n\n    Inherits exception(), __str__() from MontyError.\n    Cannot be constructed directly from Python.\n    \"\"\"\n\n    def display(\n        self,\n        format: Literal[\n            'full', 'concise', 'azure', 'json', 'jsonlines', 'rdjson', 'pylint', 'gitlab', 'github'\n        ] = 'full',\n        color: bool = False,\n    ) -> str:\n        \"\"\"Renders the type error diagnostics with the specified format and color.\n\n        Args:\n            format: Output format for the diagnostics. Defaults to 'full'.\n            color: Whether to include ANSI color codes. Defaults to False.\n        \"\"\"\n\n@final\nclass MontyRuntimeError(MontyError):\n    \"\"\"Raised when Monty code fails during execution.\n\n    Inherits exception(), __str__() from MontyError.\n    Additionally provides traceback() and display() methods.\n    \"\"\"\n\n    def traceback(self) -> list[Frame]:\n        \"\"\"Returns the Monty traceback as a list of Frame objects.\"\"\"\n\n    def display(self, format: Literal['traceback', 'type-msg', 'msg'] = 'traceback') -> str:\n        \"\"\"Returns formatted exception string.\n\n        Args:\n            format: 'traceback' - full traceback with exception\n                  'type-msg' - 'ExceptionType: message' format\n                  'msg' - just the message\n        \"\"\"\n\n@final\nclass Frame:\n    \"\"\"A single frame in a Monty traceback.\"\"\"\n\n    @property\n    def filename(self) -> str:\n        \"\"\"The filename where the code is located.\"\"\"\n\n    @property\n    def line(self) -> int:\n        \"\"\"Line number (1-based).\"\"\"\n\n    @property\n    def column(self) -> int:\n        \"\"\"Column number (1-based).\"\"\"\n\n    @property\n    def end_line(self) -> int:\n        \"\"\"End line number (1-based).\"\"\"\n\n    @property\n    def end_column(self) -> int:\n        \"\"\"End column number (1-based).\"\"\"\n\n    @property\n    def function_name(self) -> str | None:\n        \"\"\"The name of the function, or None for module-level code.\"\"\"\n\n    @property\n    def source_line(self) -> str | None:\n        \"\"\"The source code line for preview in the traceback.\"\"\"\n\n    def dict(self) -> dict[str, int | str | None]:\n        \"\"\"dict of attributes.\"\"\"\n\ndef load_snapshot(\n    data: bytes,\n    *,\n    print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n    dataclass_registry: list[type] | None = None,\n) -> FunctionSnapshot | NameLookupSnapshot | FutureSnapshot:\n    \"\"\"Load a non-REPL snapshot from serialized bytes.\n\n    Auto-detects the snapshot type (FunctionSnapshot, NameLookupSnapshot, or\n    FutureSnapshot) from the serialized data.\n\n    Arguments:\n        data: Serialized snapshot bytes from `.dump()`\n        print_callback: Optional callback for print output\n        dataclass_registry: Optional list of dataclass types to register\n\n    Returns:\n        The deserialized snapshot, ready to be resumed.\n\n    Raises:\n        ValueError: If deserialization fails or data contains a REPL snapshot\n            (use `load_repl_snapshot` for those).\n    \"\"\"\n\ndef load_repl_snapshot(\n    data: bytes,\n    *,\n    print_callback: Callable[[Literal['stdout'], str], None] | None = None,\n    dataclass_registry: list[type] | None = None,\n) -> tuple[FunctionSnapshot | NameLookupSnapshot | FutureSnapshot, MontyRepl]:\n    \"\"\"Load a REPL snapshot from serialized bytes.\n\n    Returns both the snapshot and a reconstructed `MontyRepl` session.\n    The snapshot's REPL variant is wired to the returned `MontyRepl`,\n    so resuming the snapshot will update the REPL state.\n\n    Arguments:\n        data: Serialized snapshot bytes from `.dump()` on a REPL snapshot\n        print_callback: Optional callback for print output\n        dataclass_registry: Optional list of dataclass types to register\n\n    Returns:\n        A tuple of (snapshot, MontyRepl).\n\n    Raises:\n        ValueError: If deserialization fails.\n    \"\"\"\n"
  },
  {
    "path": "crates/monty-python/python/pydantic_monty/os_access.py",
    "content": "from __future__ import annotations\n\nfrom abc import ABC, abstractmethod\nfrom pathlib import PurePosixPath\nfrom typing import TYPE_CHECKING, Any, Callable, Literal, NamedTuple, Protocol, Sequence, TypeAlias, TypeGuard\n\nif TYPE_CHECKING:\n    # Self is 3.11+, hence this\n    from typing import Self\n\n__all__ = 'OsFunction', 'AbstractOS', 'AbstractFile', 'MemoryFile', 'CallbackFile', 'OSAccess', 'StatResult'\n\nOsFunction = Literal[\n    'Path.exists',\n    'Path.is_file',\n    'Path.is_dir',\n    'Path.is_symlink',\n    'Path.read_text',\n    'Path.read_bytes',\n    'Path.write_text',\n    'Path.write_bytes',\n    'Path.mkdir',\n    'Path.unlink',\n    'Path.rmdir',\n    'Path.iterdir',\n    'Path.stat',\n    'Path.rename',\n    'Path.resolve',\n    'Path.absolute',\n    'os.getenv',\n    'os.environ',\n]\n\n\nclass StatResult(NamedTuple):\n    \"\"\"Equivalent to os.stat_result.\"\"\"\n\n    @classmethod\n    def file_stat(cls, size: int, mode: int = 0o644, mtime: float | None = None) -> Self:\n        \"\"\"Creates a stat_result namedtuple for a regular file.\n\n        Use this when responding to Path.stat() OS calls.\n\n        Args:\n            size: File size in bytes\n            mode: File permissions as octal (e.g., 0o644) or full mode with file type\n            mtime: Modification time as Unix timestamp, defaults to Now.\n\n        \"\"\"\n        import time\n\n        # If only permission bits provided (no file type), add regular file type\n        if mode < 0o1000:\n            mode = mode | 0o100_000\n        mtime = time.time() if mtime is None else mtime\n        return cls(mode, 0, 0, 1, 0, 0, size, mtime, mtime, mtime)\n\n    @classmethod\n    def dir_stat(cls, mode: int = 0o755, mtime: float | None = None) -> Self:\n        \"\"\"Creates a stat_result namedtuple for a directory.\n\n        Use this when responding to Path.stat() OS calls on directories.\n\n        Args:\n            mode: Directory permissions as octal (e.g., 0o755) or full mode with file type\n            mtime: Modification time as Unix timestamp, defaults to Now.\n\n        Returns:\n            A namedtuple with stat_result fields\n        \"\"\"\n        import time\n\n        # If only permission bits provided (no file type), add directory type\n        if mode < 0o1000:\n            mode = mode | 0o040_000\n\n        mtime = time.time() if mtime is None else mtime\n        return cls(mode, 0, 0, 2, 0, 0, 4096, mtime, mtime, mtime)\n\n    st_mode: int\n    \"\"\"protection bits\"\"\"\n\n    st_ino: int\n    \"\"\"inode\"\"\"\n\n    st_dev: int\n    \"\"\"device\"\"\"\n\n    st_nlink: int\n    \"\"\"number of hard links\"\"\"\n\n    st_uid: int\n    \"\"\"user ID of owner\"\"\"\n\n    st_gid: int\n    \"\"\"group ID of owner\"\"\"\n\n    st_size: int\n    \"\"\"total size, in bytes\"\"\"\n\n    st_atime: float\n    \"\"\"time of last access\"\"\"\n\n    st_mtime: float\n    \"\"\"time of last modification\"\"\"\n\n    st_ctime: float\n    \"\"\"time of last change\"\"\"\n\n\nclass AbstractOS(ABC):\n    \"\"\"Abstract base class for implementing virtual filesystems and OS access.\n\n    Subclass this and implement the abstract methods to provide a custom\n    filesystem that Monty code can interact with via Path methods.\n\n    Pass an instance as the `os` parameter to `Monty.run()`.\n    \"\"\"\n\n    def __call__(self, function_name: OsFunction, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        \"\"\"Dispatch a filesystem operation to the appropriate method.\n\n        This is called by Monty when Monty code invokes Path methods.\n        You typically don't need to override this method.\n\n        Args:\n            function_name: The Path method being called (e.g., 'Path.exists').\n            args: The arguments passed to the method.\n            kwargs: The keyword arguments passed to the method.\n\n        Returns:\n            The result of the filesystem operation.\n        \"\"\"\n        kwargs = kwargs or {}\n        match function_name:\n            case 'Path.exists':\n                return self.path_exists(*args)\n            case 'Path.is_file':\n                return self.path_is_file(*args)\n            case 'Path.is_dir':\n                return self.path_is_dir(*args)\n            case 'Path.is_symlink':\n                return self.path_is_symlink(*args)\n            case 'Path.read_text':\n                return self.path_read_text(*args)\n            case 'Path.read_bytes':\n                return self.path_read_bytes(*args)\n            case 'Path.write_text':\n                return self.path_write_text(*args)\n            case 'Path.write_bytes':\n                return self.path_write_bytes(*args)\n            case 'Path.mkdir':\n                assert len(kwargs) <= 2, f'Unexpected keyword arguments: {kwargs}'\n                parents = kwargs.get('parents', False)\n                exist_ok = kwargs.get('exist_ok', False)\n                return self.path_mkdir(*args, parents=parents, exist_ok=exist_ok)\n            case 'Path.unlink':\n                return self.path_unlink(*args)\n            case 'Path.rmdir':\n                return self.path_rmdir(*args)\n            case 'Path.iterdir':\n                return self.path_iterdir(*args)\n            case 'Path.stat':\n                return self.path_stat(*args)\n            case 'Path.rename':\n                return self.path_rename(*args)\n            case 'Path.resolve':\n                return self.path_resolve(*args)\n            case 'Path.absolute':\n                return self.path_absolute(*args)\n            case 'os.getenv':\n                return self.getenv(*args)\n            case 'os.environ':\n                return self.get_environ()\n\n    @abstractmethod\n    def path_exists(self, path: PurePosixPath) -> bool:\n        \"\"\"Check if a path exists.\n\n        Args:\n            path: The path to check.\n\n        Returns:\n            True if the path exists, False otherwise.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_is_file(self, path: PurePosixPath) -> bool:\n        \"\"\"Check if a path is a regular file.\n\n        Args:\n            path: The path to check.\n\n        Returns:\n            True if the path is a regular file, False otherwise.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_is_dir(self, path: PurePosixPath) -> bool:\n        \"\"\"Check if a path is a directory.\n\n        Args:\n            path: The path to check.\n\n        Returns:\n            True if the path is a directory, False otherwise.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_is_symlink(self, path: PurePosixPath) -> bool:\n        \"\"\"Check if a path is a symbolic link.\n\n        Args:\n            path: The path to check.\n\n        Returns:\n            True if the path is a symbolic link, False otherwise.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_read_text(self, path: PurePosixPath) -> str:\n        \"\"\"Read the contents of a file as text.\n\n        Args:\n            path: The path to the file.\n\n        Returns:\n            The file contents as a string.\n\n        Raises:\n            FileNotFoundError: If the file does not exist.\n            IsADirectoryError: If the path is a directory.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_read_bytes(self, path: PurePosixPath) -> bytes:\n        \"\"\"Read the contents of a file as bytes.\n\n        Args:\n            path: The path to the file.\n\n        Returns:\n            The file contents as bytes.\n\n        Raises:\n            FileNotFoundError: If the file does not exist.\n            IsADirectoryError: If the path is a directory.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_write_text(self, path: PurePosixPath, data: str) -> int:\n        \"\"\"Write text data to a file.\n\n        Args:\n            path: The path to the file.\n            data: The text content to write.\n\n        Returns:\n            The number of characters written.\n\n        Raises:\n            FileNotFoundError: If the parent directory does not exist.\n            IsADirectoryError: If the path is a directory.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_write_bytes(self, path: PurePosixPath, data: bytes) -> int:\n        \"\"\"Write binary data to a file.\n\n        Args:\n            path: The path to the file.\n            data: The binary content to write.\n\n        Returns:\n            The number of bytes written.\n\n        Raises:\n            FileNotFoundError: If the parent directory does not exist.\n            IsADirectoryError: If the path is a directory.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_mkdir(self, path: PurePosixPath, parents: bool, exist_ok: bool) -> None:\n        \"\"\"Create a directory.\n\n        Args:\n            path: The path of the directory to create.\n            parents: If True, create parent directories as needed.\n            exist_ok: If True, don't raise an error if the directory exists.\n\n        Raises:\n            FileNotFoundError: If parents is False and parent directory doesn't exist.\n            FileExistsError: If exist_ok is False and the directory already exists.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_unlink(self, path: PurePosixPath) -> None:\n        \"\"\"Remove a file.\n\n        Args:\n            path: The path to the file to remove.\n\n        Raises:\n            FileNotFoundError: If the file does not exist.\n            IsADirectoryError: If the path is a directory.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_rmdir(self, path: PurePosixPath) -> None:\n        \"\"\"Remove an empty directory.\n\n        Args:\n            path: The path to the directory to remove.\n\n        Raises:\n            FileNotFoundError: If the directory does not exist.\n            NotADirectoryError: If the path is not a directory.\n            OSError: If the directory is not empty.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_iterdir(self, path: PurePosixPath) -> list[PurePosixPath]:\n        \"\"\"List the contents of a directory.\n\n        Args:\n            path: The path to the directory.\n\n        Returns:\n            A list of full paths (as PurePosixPath) for entries in the directory.\n\n        Raises:\n            FileNotFoundError: If the directory does not exist.\n            NotADirectoryError: If the path is not a directory.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_stat(self, path: PurePosixPath) -> StatResult:\n        \"\"\"Get file status information.\n\n        Use file_stat(), dir_stat(), or symlink_stat() helpers to create the return value.\n\n        Args:\n            path: The path to stat.\n\n        Returns:\n            A StatResult with file metadata.\n\n        Raises:\n            FileNotFoundError: If the path does not exist.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_rename(self, path: PurePosixPath, target: PurePosixPath) -> None:\n        \"\"\"Rename a file or directory.\n\n        Args:\n            path: The current path.\n            target: The new path.\n\n        Raises:\n            FileNotFoundError: If the source path does not exist.\n            FileExistsError: If the target already exists (platform-dependent).\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_resolve(self, path: PurePosixPath) -> str:\n        \"\"\"Resolve a path to an absolute path, resolving any symlinks.\n\n        Args:\n            path: The path to resolve.\n\n        Returns:\n            The resolved absolute path with symlinks resolved.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def path_absolute(self, path: PurePosixPath) -> str:\n        \"\"\"Convert a path to an absolute path without resolving symlinks.\n\n        Args:\n            path: The path to convert.\n\n        Returns:\n            The absolute path.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def getenv(self, key: str, default: str | None = None) -> str | None:\n        \"\"\"Get an environment variable value.\n\n        Args:\n            key: The name of the environment variable.\n            default: The value to return if the environment variable is not set.\n\n        Returns:\n            The value of the environment variable, or `default` if not set.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def get_environ(self) -> dict[str, str]:\n        \"\"\"Get the entire environment as a dictionary.\n\n        Returns:\n            A dictionary containing all environment variables.\n        \"\"\"\n        raise NotImplementedError\n\n\nclass AbstractFile(Protocol):\n    \"\"\"Protocol defining the interface for files used with OSAccess.\n\n    This protocol allows custom file implementations to be used with OSAccess.\n    The built-in implementations are:\n\n    - `MemoryFile`: Stores content in memory (recommended for sandboxed execution)\n    - `CallbackFile`: Delegates to custom callbacks (use with caution - see its docstring)\n\n    Security Note:\n        Custom implementations of this protocol run in the host Python environment.\n        The `read_content()` and `write_content()` methods can execute arbitrary code,\n        including accessing the real filesystem. Only use implementations you trust.\n\n        For sandboxed execution where Monty code should not access real files,\n        use `MemoryFile` which stores all content in memory.\n\n    Attributes:\n        path: The virtual path of the file within the OSAccess filesystem.\n        name: The filename (basename) extracted from path.\n        permissions: Unix-style permission bits (e.g., 0o644).\n        deleted: Whether the file has been marked as deleted.\n    \"\"\"\n\n    path: PurePosixPath\n    name: str\n    permissions: int\n    deleted: bool\n\n    def read_content(self) -> str | bytes:\n        \"\"\"Read and return the file's content.\"\"\"\n        ...\n\n    def write_content(self, content: str | bytes) -> None:\n        \"\"\"Write content to the file.\"\"\"\n        ...\n\n    def delete(self) -> None:\n        \"\"\"Mark the file as deleted.\"\"\"\n        ...\n\n\nTree: TypeAlias = 'dict[str, AbstractFile | Tree]'\n\n\ndef _is_file(entry: None | AbstractFile | Tree) -> TypeGuard[AbstractFile]:\n    return hasattr(entry, 'path')\n\n\ndef _is_dir(entry: None | AbstractFile | Tree) -> TypeGuard[Tree]:\n    return isinstance(entry, dict)\n\n\nclass MemoryFile:\n    \"\"\"An in-memory virtual file for use with OSAccess.\n\n    This is the recommended file type for sandboxed Monty execution. Content is\n    stored entirely in Python memory with no access to the real filesystem.\n\n    When Monty code reads from this file, it receives the stored content.\n    When Monty code writes to this file, the content attribute is updated.\n\n    Example::\n\n        from pydantic_monty import Monty, OSAccess, MemoryFile\n\n        fs = OSAccess(\n            [\n                MemoryFile('/config.json', '{\"debug\": true}'),\n                MemoryFile('/data.bin', b'\\\\x00\\\\x01\\\\x02'),\n            ]\n        )\n\n        result = Monty('''\n            from pathlib import Path\n            Path('/config.json').read_text()\n        ''').run(os=fs)\n        # result == '{\"debug\": true}'\n\n    Attributes:\n        path: The virtual path of the file within the OSAccess filesystem.\n        name: The filename (basename) extracted from path.\n        content: The file content (str for text, bytes for binary).\n        permissions: Unix-style permission bits (default: 0o644).\n        deleted: Whether the file has been marked as deleted.\n    \"\"\"\n\n    path: PurePosixPath\n    name: str\n    content: str | bytes\n    permissions: int = 0o644\n    deleted: bool\n\n    def __init__(self, path: str | PurePosixPath, content: str | bytes, *, permissions: int = 0o644) -> None:\n        \"\"\"Create an in-memory virtual file.\n\n        Args:\n            path: The virtual path for this file in the OSAccess filesystem.\n            content: The initial file content (str for text, bytes for binary).\n            permissions: Unix-style permission bits (default: 0o644).\n        \"\"\"\n        self.path = PurePosixPath(path)\n        self.name = self.path.name\n        self.content = content\n        self.permissions = permissions\n        self.deleted = False\n\n    def read_content(self) -> str | bytes:\n        \"\"\"Return the stored content.\"\"\"\n        return self.content\n\n    def write_content(self, content: str | bytes) -> None:\n        \"\"\"Update the stored content.\"\"\"\n        self.content = content\n\n    def delete(self) -> None:\n        \"\"\"Mark the file as deleted.\"\"\"\n        self.deleted = True\n\n    def __repr__(self) -> str:\n        repr_content = \"'...'\" if isinstance(self.content, str) else \"b'...'\"\n        return f'MemoryFile(path={self.path}, content={repr_content}, permissions={self.permissions})'\n\n\n_type_check_memory_file: AbstractFile = MemoryFile('test.txt', '')\n\n\nclass CallbackFile:\n    \"\"\"A virtual file backed by custom read/write callbacks.\n\n    This class allows you to create files whose content is dynamically generated\n    or persisted through custom logic. When Monty code reads or writes to this file,\n    the provided callbacks are invoked.\n\n    Security Warning:\n        The callbacks execute in the host Python environment with FULL access to\n        the real filesystem, network, and all system resources. A callback that\n        accesses the real filesystem effectively breaks the Monty sandbox.\n\n        Example of UNSAFE usage that breaks the sandbox::\n\n            # DON'T DO THIS - allows Monty to read real files!\n            CallbackFile(\n                '/config.txt',\n                read=lambda p: open('/etc/passwd').read(),\n                write=lambda p, c: open('/tmp/out', 'w').write(c),\n            )\n\n        For sandboxed execution, use `MemoryFile` instead, which stores content\n        purely in memory with no external access.\n\n    Safe use cases for CallbackFile:\n        - Returning dynamically computed content (e.g., current timestamp)\n        - Logging writes without persisting them\n        - Validating/transforming content before storage in memory\n        - Integration testing with controlled external resources\n\n    Attributes:\n        path: The virtual path of the file within the OSAccess filesystem.\n        name: The filename (basename) extracted from path.\n        read: Callback invoked when the file is read. Receives the path and\n            must return str or bytes.\n        write: Callback invoked when the file is written. Receives the path\n            and content (str or bytes).\n        permissions: Unix-style permission bits (default: 0o644).\n        deleted: Whether the file has been marked as deleted.\n    \"\"\"\n\n    path: PurePosixPath\n    name: str\n    read: Callable[[PurePosixPath], str | bytes]\n    write: Callable[[PurePosixPath, str | bytes], None]\n    permissions: int = 0o644\n    deleted: bool\n\n    def __init__(\n        self,\n        path: str | PurePosixPath,\n        read: Callable[[PurePosixPath], str | bytes],\n        write: Callable[[PurePosixPath, str | bytes], None],\n        *,\n        permissions: int = 0o644,\n    ) -> None:\n        \"\"\"Create a callback-backed virtual file.\n\n        Args:\n            path: The virtual path for this file in the OSAccess filesystem.\n            read: Callback to generate content when the file is read.\n            write: Callback to handle content when the file is written.\n            permissions: Unix-style permission bits (default: 0o644).\n        \"\"\"\n        self.path = PurePosixPath(path)\n        self.name = self.path.name\n        self.read = read\n        self.write = write\n        self.permissions = permissions\n        self.deleted = False\n\n    def read_content(self) -> str | bytes:\n        \"\"\"Read content by invoking the read callback.\"\"\"\n        return self.read(self.path)\n\n    def write_content(self, content: str | bytes) -> None:\n        \"\"\"Write content by invoking the write callback.\"\"\"\n        self.write(self.path, content)\n\n    def delete(self) -> None:\n        \"\"\"Mark the file as deleted.\"\"\"\n        self.deleted = True\n\n    def __repr__(self) -> str:\n        return f'CallbackFile(path={self.path}, read={self.read}, write={self.write}, permissions={self.permissions})'\n\n\n_type_check_callback_file: AbstractFile = CallbackFile('test.txt', lambda _: '', lambda _, __: None)\n\n\nclass OSAccess(AbstractOS):\n    \"\"\"In-memory virtual filesystem for sandboxed Monty execution.\n\n    OSAccess provides a complete virtual filesystem that Monty code can interact\n    with via `pathlib.Path` methods. Files exist only in memory (when using\n    `MemoryFile`) and cannot access the real filesystem.\n\n    Security Model:\n        When using `MemoryFile` objects, OSAccess is fully sandboxed:\n\n        - Monty code can only access files explicitly registered with OSAccess\n        - Path traversal (e.g., `../../etc/passwd`) cannot escape to real files\n        - All file content is stored in Python memory, not on disk\n        - Environment variables are isolated to the provided `environ` dict\n\n        However, if `CallbackFile` is used, the callbacks run in the host\n        environment and CAN access real resources. See `CallbackFile` docstring.\n\n    Attributes:\n        files: List of AbstractFile objects registered with this filesystem.\n        environ: Dictionary of environment variables accessible via os.getenv().\n    \"\"\"\n\n    files: list[AbstractFile]\n    environ: dict[str, str]\n    _tree: Tree\n\n    def __init__(\n        self,\n        files: Sequence[AbstractFile] | None = None,\n        environ: dict[str, str] | None = None,\n        *,\n        root_dir: str | PurePosixPath = '/',\n    ):\n        \"\"\"Create a virtual filesystem with the given files.\n\n        Args:\n            files: Files to register in the virtual filesystem. Use `MemoryFile`\n                for sandboxed in-memory files, or `CallbackFile` for custom logic\n                (with security caveats - see its docstring).\n            environ: Environment variables accessible to Monty code via os.getenv().\n                Isolated from the real environment.\n            root_dir: Base directory for normalizing relative file paths. Relative\n                paths in files will be prefixed with this. Default is '/'.\n\n        Raises:\n            AssertionError: If root_dir is not an absolute path.\n            ValueError: If a file path conflicts with another file (e.g., trying\n                to create a file inside another file's path).\n        \"\"\"\n        self.files = list(files) if files else []\n        self.environ = environ or {}\n        # Initialize tree with root directory - / is always present\n        self._tree = {'/': {}}\n        root_dir = PurePosixPath(root_dir)\n        assert root_dir.is_absolute(), f'Root directory must be absolute, got {root_dir}'\n        for file in self.files:\n            if not file.path.is_absolute():\n                file.path = root_dir / file.path\n\n            subtree = self._tree\n            *dir_parts, name = file.path.parts\n            for part in dir_parts:\n                entry = subtree.setdefault(part, {})\n                if _is_dir(entry):\n                    subtree = entry\n                else:\n                    raise ValueError(f'Cannot put file {file} within sub-directory of file {entry}')\n\n            subtree[name] = file\n\n    def __repr__(self) -> str:\n        return f'OSAccess(files={self.files}, environ={self.environ})'\n\n    def path_exists(self, path: PurePosixPath) -> bool:\n        return self._get_entry(path) is not None\n\n    def path_is_file(self, path: PurePosixPath) -> bool:\n        return _is_file(self._get_entry(path))\n\n    def path_is_dir(self, path: PurePosixPath) -> bool:\n        return _is_dir(self._get_entry(path))\n\n    def path_is_symlink(self, path: PurePosixPath) -> bool:\n        return False\n\n    def path_read_text(self, path: PurePosixPath) -> str:\n        file = self._get_file(path)\n        content = file.read_content()\n        return content if isinstance(content, str) else content.decode()\n\n    def path_read_bytes(self, path: PurePosixPath) -> bytes:\n        file = self._get_file(path)\n        content = file.read_content()\n        return content if isinstance(content, bytes) else content.encode()\n\n    def path_write_text(self, path: PurePosixPath, data: str) -> int:\n        self._write_file(path, data)\n        return len(data)\n\n    def path_write_bytes(self, path: PurePosixPath, data: bytes) -> int:\n        self._write_file(path, data)\n        return len(data)\n\n    def _write_file(self, path: PurePosixPath, data: bytes | str) -> None:\n        entry = self._get_entry(path)\n        if _is_file(entry):\n            entry.write_content(data)\n            return\n        elif _is_dir(entry):\n            raise IsADirectoryError(f'[Errno 21] Is a directory: {str(path)!r}')\n\n        # write a new file if the parent directory exists\n        parent_entry = self._parent_entry(path)\n        if _is_dir(parent_entry):\n            file_path = PurePosixPath(path)\n            parent_entry[file_path.name] = new_file = MemoryFile(file_path, data)\n            self.files.append(new_file)\n        else:\n            raise FileNotFoundError(f'[Errno 2] No such file or directory: {str(path)!r}')\n\n    def path_mkdir(self, path: PurePosixPath, parents: bool, exist_ok: bool) -> None:\n        entry = self._get_entry(path)\n        if _is_file(entry):\n            raise FileExistsError(f'[Errno 17] File exists: {str(path)!r}')\n        elif _is_dir(entry):\n            if exist_ok:\n                return\n            else:\n                raise FileExistsError(f'[Errno 17] File exists: {str(path)!r}')\n\n        parent_entry = self._parent_entry(path)\n        if _is_dir(parent_entry):\n            parent_entry[PurePosixPath(path).name] = {}\n            return\n        elif _is_file(parent_entry):\n            raise NotADirectoryError(f'[Errno 20] Not a directory: {str(path)!r}')\n        elif parents:\n            subtree = self._tree\n            for part in PurePosixPath(path).parts:\n                entry = subtree.setdefault(part, {})\n                if _is_dir(entry):\n                    subtree = entry\n                else:\n                    raise NotADirectoryError(f'[Errno 20] Not a directory: {str(path)!r}')\n        else:\n            raise FileNotFoundError(f'[Errno 2] No such file or directory: {str(path)!r}')\n\n    def path_unlink(self, path: PurePosixPath) -> None:\n        file = self._get_file(path)\n        file.delete()\n        # remove from parent\n        parent_dir = self._parent_entry(path)\n        assert _is_dir(parent_dir), f'Expected parent of a file to always be a directory, got {parent_dir}'\n        del parent_dir[file.name]\n\n    def path_rmdir(self, path: PurePosixPath) -> None:\n        dir = self._get_dir(path)\n        if dir:\n            raise OSError(f'[Errno 39] Directory not empty: {str(path)!r}')\n        # remove from parent\n        parent_dir = self._parent_entry(path)\n        assert _is_dir(parent_dir), f'Expected parent of a file to always be a directory, got {parent_dir}'\n        del parent_dir[PurePosixPath(path).name]\n\n    def path_iterdir(self, path: PurePosixPath) -> list[PurePosixPath]:\n        # Return full paths as PurePosixPath objects (will be converted to MontyObject::Path)\n        dir_path = PurePosixPath(path)\n        return [dir_path / name for name in self._get_dir(path).keys()]\n\n    def path_stat(self, path: PurePosixPath) -> StatResult:\n        entry = self._get_entry_exists(path)\n        if _is_file(entry):\n            content = entry.read_content()\n            size = len(content) if isinstance(content, bytes) else len(content.encode())\n            return StatResult.file_stat(size=size, mode=entry.permissions)\n        else:\n            return StatResult.dir_stat()\n\n    def path_rename(self, path: PurePosixPath, target: PurePosixPath) -> None:\n        src_entry = self._get_entry(path)\n        if src_entry is None:\n            raise FileNotFoundError(f'[Errno 2] No such file or directory: {str(path)!r} -> {str(target)!r}')\n\n        parent_dir = self._parent_entry(path)\n        assert _is_dir(parent_dir), f'Expected parent of a file to always be a directory, got {parent_dir}'\n\n        target_parent = self._parent_entry(target)\n        if not _is_dir(target_parent):\n            raise FileNotFoundError(f'[Errno 2] No such file or directory: {str(path)!r} -> {str(target)!r}')\n        target_entry = self._get_entry(target)\n\n        if _is_file(src_entry):\n            if _is_dir(target_entry):\n                raise IsADirectoryError(f'[Errno 21] Is a directory: {str(path)!r} -> {str(target)!r}')\n            if _is_file(target_entry):\n                # need to mark the target as deleted as it'll be overwritten\n                target_entry.delete()\n\n            src_name = src_entry.path.name\n            target_name = PurePosixPath(target).name\n            # remove it from the old directory\n            del parent_dir[src_name]\n            # and put it in the new directory\n            target_parent[target_name] = src_entry\n        else:\n            assert _is_dir(src_entry), 'src path must be a directory here'\n            if _is_file(target_entry):\n                raise NotADirectoryError(f'[Errno 20] Not a directory: {str(path)!r} -> {str(target)!r}')\n            elif _is_dir(target_entry) and target_entry:\n                raise OSError(f'[Errno 66] Directory not empty: {str(path)!r} -> {str(target)!r}')\n\n            src_name = PurePosixPath(path).name\n            target_name = PurePosixPath(target).name\n            # remove it from the old directory\n            del parent_dir[src_name]\n            # and put it in the new directory\n            target_parent[target_name] = src_entry\n\n            # Update paths for all files in the renamed directory\n            self._update_paths_recursive(src_entry, PurePosixPath(path), PurePosixPath(target))\n\n    def path_resolve(self, path: PurePosixPath) -> str:\n        # No symlinks in OSAccess, so resolve is same as absolute with normalization\n        return self.path_absolute(path)\n\n    def path_absolute(self, path: PurePosixPath) -> str:\n        p = PurePosixPath(path)\n        if p.is_absolute():\n            return str(p)\n        # In this virtual filesystem, we treat '/' as the working directory\n        return str(PurePosixPath('/') / p)\n\n    def getenv(self, key: str, default: str | None = None) -> str | None:\n        return self.environ.get(key, default)\n\n    def get_environ(self) -> dict[str, str]:\n        return self.environ\n\n    def _get_entry(self, path: PurePosixPath) -> Tree | AbstractFile | None:\n        dir = self._tree\n\n        *dir_parts, name = PurePosixPath(path).parts\n\n        for part in dir_parts:\n            entry = dir.get(part)\n            if _is_dir(entry):\n                dir = entry\n            else:\n                return None\n\n        return dir.get(name)\n\n    def _get_entry_exists(self, path: PurePosixPath) -> Tree | AbstractFile:\n        entry = self._get_entry(path)\n        if entry is None:\n            raise FileNotFoundError(f'[Errno 2] No such file or directory: {str(path)!r}')\n        else:\n            return entry\n\n    def _get_file(self, path: PurePosixPath) -> AbstractFile:\n        entry = self._get_entry_exists(path)\n        if _is_file(entry):\n            return entry\n        else:\n            raise IsADirectoryError(f'[Errno 21] Is a directory: {str(path)!r}')\n\n    def _get_dir(self, path: PurePosixPath) -> Tree:\n        entry = self._get_entry_exists(path)\n        if _is_dir(entry):\n            return entry\n        else:\n            raise NotADirectoryError(f'[Errno 20] Not a directory: {str(path)!r}')\n\n    def _parent_entry(self, path: PurePosixPath) -> Tree | AbstractFile | None:\n        return self._get_entry(PurePosixPath(path).parent)\n\n    def _update_paths_recursive(self, tree: Tree, old_prefix: PurePosixPath, new_prefix: PurePosixPath) -> None:\n        \"\"\"Update path attributes for all files in a tree after directory rename.\n\n        When a directory is renamed, the internal tree structure is moved but\n        AbstractFile objects still have their old paths. This method recursively\n        updates all file paths by replacing old_prefix with new_prefix.\n        \"\"\"\n        for entry in tree.values():\n            if _is_file(entry):\n                # Replace old prefix with new prefix in file path\n                relative = entry.path.relative_to(old_prefix)\n                entry.path = new_prefix / relative\n            elif _is_dir(entry):\n                self._update_paths_recursive(entry, old_prefix, new_prefix)\n"
  },
  {
    "path": "crates/monty-python/python/pydantic_monty/py.typed",
    "content": ""
  },
  {
    "path": "crates/monty-python/src/convert.rs",
    "content": "//! Type conversion between Monty's `MontyObject` and PyO3 Python objects.\n//!\n//! This module provides bidirectional conversion:\n//! - `py_to_monty`: Convert Python objects to Monty's `MontyObject` for input\n//! - `monty_to_py`: Convert Monty's `MontyObject` back to Python objects for output\n\nuse ::monty::MontyObject;\nuse monty::MontyException;\nuse num_bigint::BigInt;\nuse pyo3::{\n    exceptions::{PyBaseException, PyTypeError},\n    intern,\n    prelude::*,\n    sync::PyOnceLock,\n    types::{PyBool, PyBytes, PyDict, PyFloat, PyFrozenSet, PyInt, PyList, PySet, PyString, PyTuple},\n};\n\nuse crate::{\n    dataclass::{DcRegistry, dataclass_to_monty, dataclass_to_py, is_dataclass},\n    exceptions::{exc_monty_to_py, exc_to_monty_object},\n};\n\n/// Converts a Python object to Monty's `MontyObject` representation.\n///\n/// Handles all standard Python types that Monty supports as inputs, including callable objects\n/// which are converted to `MontyObject::Function`. Unsupported types will raise a `TypeError`.\n///\n/// When a dataclass is encountered, it is automatically registered in `dc_registry`\n/// so that the original Python type can be reconstructed on output (enabling `isinstance()`).\n/// This applies recursively to nested dataclasses in fields, lists, dicts, etc.\n///\n/// # Important\n/// Checks `bool` before `int` since `bool` is a subclass of `int` in Python.\n/// Callable check is last since many Python types (classes, etc.) are technically callable.\npub fn py_to_monty(obj: &Bound<'_, PyAny>, dc_registry: &DcRegistry) -> PyResult<MontyObject> {\n    if obj.is_none() {\n        Ok(MontyObject::None)\n    } else if let Ok(bool) = obj.cast::<PyBool>() {\n        // Check bool BEFORE int since bool is a subclass of int in Python\n        Ok(MontyObject::Bool(bool.is_true()))\n    } else if let Ok(int) = obj.cast::<PyInt>() {\n        // Try i64 first (fast path), fall back to BigInt for large values\n        if let Ok(i) = int.extract::<i64>() {\n            Ok(MontyObject::Int(i))\n        } else {\n            // Extract as BigInt for values that don't fit in i64\n            let bi: BigInt = int.extract()?;\n            Ok(MontyObject::BigInt(bi))\n        }\n    } else if let Ok(float) = obj.cast::<PyFloat>() {\n        Ok(MontyObject::Float(float.extract()?))\n    } else if let Ok(string) = obj.cast::<PyString>() {\n        Ok(MontyObject::String(string.extract()?))\n    } else if let Ok(bytes) = obj.cast::<PyBytes>() {\n        Ok(MontyObject::Bytes(bytes.extract()?))\n    } else if let Ok(list) = obj.cast::<PyList>() {\n        let items: PyResult<Vec<MontyObject>> = list.iter().map(|item| py_to_monty(&item, dc_registry)).collect();\n        Ok(MontyObject::List(items?))\n    } else if let Ok(tuple) = obj.cast::<PyTuple>() {\n        // Check for namedtuple BEFORE treating as regular tuple\n        // Namedtuples have a `_fields` attribute with field names\n        if let Ok(fields) = obj.getattr(\"_fields\")\n            && let Ok(fields_tuple) = fields.cast::<PyTuple>()\n        {\n            let py_type = obj.get_type();\n            // Get the simple class name (e.g., \"stat_result\")\n            let simple_name = py_type.name()?.to_string();\n            // Get the module (e.g., \"os\" or \"__main__\")\n            let module: String = py_type.getattr(\"__module__\")?.extract()?;\n            // Construct full type name: \"os.stat_result\"\n            // Skip module prefix if it's a Python built-in module\n            let type_name = if module.starts_with('_') || module == \"builtins\" {\n                simple_name\n            } else {\n                format!(\"{module}.{simple_name}\")\n            };\n            // Extract field names as strings\n            let field_names: PyResult<Vec<String>> = fields_tuple.iter().map(|f| f.extract::<String>()).collect();\n            // Extract values\n            let values: PyResult<Vec<MontyObject>> = tuple.iter().map(|item| py_to_monty(&item, dc_registry)).collect();\n            return Ok(MontyObject::NamedTuple {\n                type_name,\n                field_names: field_names?,\n                values: values?,\n            });\n        }\n        // Regular tuple\n        let items: PyResult<Vec<MontyObject>> = tuple.iter().map(|item| py_to_monty(&item, dc_registry)).collect();\n        Ok(MontyObject::Tuple(items?))\n    } else if let Ok(dict) = obj.cast::<PyDict>() {\n        // in theory we could provide a way of passing the iterator direct to the internal MontyObject construct\n        // it's probably not worth it right now\n        Ok(MontyObject::dict(\n            dict.iter()\n                .map(|(k, v)| Ok((py_to_monty(&k, dc_registry)?, py_to_monty(&v, dc_registry)?)))\n                .collect::<PyResult<Vec<(MontyObject, MontyObject)>>>()?,\n        ))\n    } else if let Ok(set) = obj.cast::<PySet>() {\n        let items: PyResult<Vec<MontyObject>> = set.iter().map(|item| py_to_monty(&item, dc_registry)).collect();\n        Ok(MontyObject::Set(items?))\n    } else if let Ok(frozenset) = obj.cast::<PyFrozenSet>() {\n        let items: PyResult<Vec<MontyObject>> = frozenset.iter().map(|item| py_to_monty(&item, dc_registry)).collect();\n        Ok(MontyObject::FrozenSet(items?))\n    } else if obj.is(obj.py().Ellipsis()) {\n        Ok(MontyObject::Ellipsis)\n    } else if let Ok(exc) = obj.cast::<PyBaseException>() {\n        Ok(exc_to_monty_object(exc))\n    } else if is_dataclass(obj) {\n        // Auto-register the dataclass type so it can be reconstructed on output\n        dc_registry.insert(&obj.get_type())?;\n        dataclass_to_monty(obj, dc_registry)\n    } else if obj.is_instance(get_pure_posix_path(obj.py())?)? {\n        // Handle pathlib.PurePosixPath and thereby pathlib.PosixPath objects\n        let path_str: String = obj.str()?.extract()?;\n        Ok(MontyObject::Path(path_str))\n    } else if obj.is_callable() {\n        // Callable check is last since many Python types (classes, etc.) are technically callable,\n        // and we want to match more specific types first (e.g. dataclasses).\n        let name = get_name(obj);\n        let docstring = get_docstring(obj);\n        Ok(MontyObject::Function { name, docstring })\n    } else if let Ok(name) = obj.get_type().qualname() {\n        let msg = match obj.get_type().module() {\n            Ok(module) => format!(\"Cannot convert {module}.{name} to Monty value\"),\n            Err(_) => format!(\"Cannot convert {name} to Monty value\"),\n        };\n        Err(PyTypeError::new_err(msg))\n    } else {\n        Err(PyTypeError::new_err(\"Cannot convert unknown type to Monty value\"))\n    }\n}\n\n/// Converts Monty's `MontyObject` to a native Python object, using the dataclass registry.\n///\n/// When a dataclass is converted and its class name is found in the registry,\n/// an instance of the original Python type is created (so `isinstance()` works).\n/// Otherwise, falls back to `PyMontyDataclass`.\npub fn monty_to_py(py: Python<'_>, obj: &MontyObject, dc_registry: &DcRegistry) -> PyResult<Py<PyAny>> {\n    match obj {\n        MontyObject::None => Ok(py.None()),\n        MontyObject::Ellipsis => Ok(py.Ellipsis()),\n        MontyObject::Bool(b) => Ok(PyBool::new(py, *b).to_owned().into_any().unbind()),\n        MontyObject::Int(i) => Ok(i.into_pyobject(py)?.clone().into_any().unbind()),\n        MontyObject::BigInt(bi) => Ok(bi.into_pyobject(py)?.clone().into_any().unbind()),\n        MontyObject::Float(f) => Ok(f.into_pyobject(py)?.clone().into_any().unbind()),\n        MontyObject::String(s) => Ok(PyString::new(py, s).into_any().unbind()),\n        MontyObject::Bytes(b) => Ok(PyBytes::new(py, b).into_any().unbind()),\n        MontyObject::List(items) => {\n            let py_items: PyResult<Vec<Py<PyAny>>> =\n                items.iter().map(|item| monty_to_py(py, item, dc_registry)).collect();\n            Ok(PyList::new(py, py_items?)?.into_any().unbind())\n        }\n        MontyObject::Tuple(items) => {\n            let py_items: PyResult<Vec<Py<PyAny>>> =\n                items.iter().map(|item| monty_to_py(py, item, dc_registry)).collect();\n            Ok(PyTuple::new(py, py_items?)?.into_any().unbind())\n        }\n        // NamedTuple - create a proper Python namedtuple using collections.namedtuple\n        MontyObject::NamedTuple {\n            type_name,\n            field_names,\n            values,\n        } => {\n            // Extract module and simple name from full type_name\n            // e.g., \"os.stat_result\" -> module=\"os\", simple_name=\"stat_result\"\n            let (module, simple_name) = if let Some(idx) = type_name.rfind('.') {\n                (&type_name[..idx], &type_name[idx + 1..])\n            } else {\n                (\"\", type_name.as_str())\n            };\n\n            // Create a namedtuple type with the module set for round-trip support\n            // collections.namedtuple(typename, field_names, module=module)\n            let namedtuple_fn = get_namedtuple(py)?;\n            let py_field_names = PyList::new(py, field_names)?;\n            let nt_type = if module.is_empty() {\n                namedtuple_fn.call1((simple_name, py_field_names))?\n            } else {\n                let kwargs = PyDict::new(py);\n                kwargs.set_item(\"module\", module)?;\n                namedtuple_fn.call((simple_name, py_field_names), Some(&kwargs))?\n            };\n\n            // Convert values and instantiate using _make() which accepts an iterable\n            // note `_make` might start with an underscore, but it's a public documented method\n            // https://docs.python.org/3/library/collections.html#collections.somenamedtuple._make\n            let py_values: PyResult<Vec<Py<PyAny>>> =\n                values.iter().map(|item| monty_to_py(py, item, dc_registry)).collect();\n            let instance = nt_type.call_method1(\"_make\", (py_values?,))?;\n            Ok(instance.into_any().unbind())\n        }\n        MontyObject::Dict(map) => {\n            let dict = PyDict::new(py);\n            for (k, v) in map {\n                dict.set_item(monty_to_py(py, k, dc_registry)?, monty_to_py(py, v, dc_registry)?)?;\n            }\n            Ok(dict.into_any().unbind())\n        }\n        MontyObject::Set(items) => {\n            let set = PySet::empty(py)?;\n            for item in items {\n                set.add(monty_to_py(py, item, dc_registry)?)?;\n            }\n            Ok(set.into_any().unbind())\n        }\n        MontyObject::FrozenSet(items) => {\n            let py_items: PyResult<Vec<Py<PyAny>>> =\n                items.iter().map(|item| monty_to_py(py, item, dc_registry)).collect();\n            Ok(PyFrozenSet::new(py, &py_items?)?.into_any().unbind())\n        }\n        // Return the exception instance as a value (not raised)\n        MontyObject::Exception { exc_type, arg } => {\n            let exc = exc_monty_to_py(py, MontyException::new(*exc_type, arg.clone()));\n            Ok(exc.into_value(py).into_any())\n        }\n        // Return Python's built-in type object\n        MontyObject::Type(t) => import_builtins(py)?.getattr(py, t.to_string()),\n        MontyObject::BuiltinFunction(f) => import_builtins(py)?.getattr(py, f.to_string()),\n        // Dataclass - use registry to reconstruct original type if available\n        MontyObject::Dataclass {\n            name,\n            type_id,\n            field_names,\n            attrs,\n            frozen,\n        } => dataclass_to_py(py, name, *type_id, field_names, attrs, *frozen, dc_registry),\n        // Path - convert to Python pathlib.Path\n        MontyObject::Path(p) => {\n            let pure_posix_path = get_pure_posix_path(py)?;\n            let path_obj = pure_posix_path.call1((p,))?;\n            Ok(path_obj.into_any().unbind())\n        }\n        // Output-only types - convert to string representation\n        MontyObject::Repr(s) => Ok(PyString::new(py, s).into_any().unbind()),\n        MontyObject::Cycle(_, placeholder) => Ok(PyString::new(py, placeholder).into_any().unbind()),\n        // Function objects are internal to the name lookup protocol and should not normally\n        // appear as final output values. If they do, represent as a string with the function name.\n        MontyObject::Function { name, .. } => Ok(PyString::new(py, name).into_any().unbind()),\n    }\n}\n\npub fn import_builtins(py: Python<'_>) -> PyResult<&Py<PyModule>> {\n    static BUILTINS: PyOnceLock<Py<PyModule>> = PyOnceLock::new();\n\n    BUILTINS.get_or_try_init(py, || py.import(\"builtins\").map(Bound::unbind))\n}\n\n/// Cached import of `collections.namedtuple` function.\nfn get_namedtuple(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static NAMEDTUPLE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    NAMEDTUPLE.import(py, \"collections\", \"namedtuple\")\n}\n\n/// Cached import of `pathlib.PurePosixPath` class.\nfn get_pure_posix_path(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static PUREPOSIX: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    PUREPOSIX.import(py, \"pathlib\", \"PurePosixPath\")\n}\n\npub fn get_name(f: &Bound<'_, PyAny>) -> String {\n    f.getattr(intern!(f.py(), \"__name__\"))\n        .and_then(|n| n.extract::<String>())\n        .unwrap_or_else(|_| \"<unknown>\".to_string())\n}\n\n/// get the `__doc__` attribute from a (hopefully) function\npub fn get_docstring(f: &Bound<'_, PyAny>) -> Option<String> {\n    f.getattr(intern!(f.py(), \"__doc__\"))\n        .and_then(|d| d.extract::<String>())\n        .ok()\n}\n"
  },
  {
    "path": "crates/monty-python/src/dataclass.rs",
    "content": "//! Dataclass conversion between Python and Monty.\n//!\n//! This module handles:\n//! - Converting Python dataclass instances to `MontyObject::Dataclass`\n//! - Converting `MontyObject::Dataclass` back to Python via `PyUnknownDataclass`\n//! - `PyUnknownDataclass`: A Python class that mimics dataclass behavior\n\nuse std::{\n    collections::hash_map::DefaultHasher,\n    hash::{Hash, Hasher},\n};\n\nuse ::monty::{DictPairs, MontyObject};\nuse pyo3::{\n    Bound,\n    exceptions::{PyAttributeError, PyTypeError},\n    intern,\n    prelude::*,\n    sync::PyOnceLock,\n    types::{PyDict, PyList, PyString, PyType},\n};\n\nuse crate::convert::{monty_to_py, py_to_monty};\n\n/// Checks if a Python object is a dataclass instance (not a type).\n///\n/// Copied from pydantic's `is_dataclass` logic.\npub fn is_dataclass(value: &Bound<'_, PyAny>) -> bool {\n    value\n        .hasattr(intern!(value.py(), \"__dataclass_fields__\"))\n        .unwrap_or(false)\n        && !value.is_instance_of::<PyType>()\n}\n\n/// Converts a Python dataclass instance to `MontyObject::Dataclass`.\n///\n/// Extracts field names in definition order (for repr) and all field values as attrs.\n/// The `type_id` is set to `id(type(dc))` in Python, allowing registry lookups by type identity.\n/// The `dc_registry` is threaded through to `py_to_monty` so that nested dataclasses\n/// in field values are also auto-registered.\npub fn dataclass_to_monty(value: &Bound<'_, PyAny>, dc_registry: &DcRegistry) -> PyResult<MontyObject> {\n    let py = value.py();\n\n    let dc_type = value.get_type();\n    let name: String = dc_type.getattr(intern!(py, \"__name__\"))?.extract()?;\n\n    // Get type_id from id(type(dc)) for registry lookups\n    let type_id = dc_type.as_ptr() as u64;\n\n    let fields_dict = value\n        .getattr(intern!(py, \"__dataclass_fields__\"))?\n        .cast_into::<PyDict>()?;\n\n    let frozen = value\n        .getattr(intern!(py, \"__dataclass_params__\"))?\n        .getattr(intern!(py, \"frozen\"))?\n        .extract::<bool>()?;\n\n    let field_type_marker = get_field_marker(py)?;\n\n    // Collect field names and attrs\n    let mut field_names = Vec::new();\n    let mut attrs = Vec::new();\n\n    for (field_name_obj, field) in fields_dict.iter() {\n        let field_type = field.getattr(intern!(py, \"_field_type\"))?;\n        if field_type.is(field_type_marker) {\n            let field_name_str = field_name_obj.cast::<PyString>()?.to_str()?.to_string();\n\n            // we don't include private fields in the dataclass serialized for monty\n            if field_name_str.starts_with('_') {\n                continue;\n            }\n\n            let field_value = value.getattr(field_name_obj.cast::<PyString>()?)?;\n            let field_name_monty = py_to_monty(&field_name_obj, dc_registry)?;\n            let field_value_monty = py_to_monty(&field_value, dc_registry)?;\n\n            field_names.push(field_name_str);\n            attrs.push((field_name_monty, field_value_monty));\n        }\n    }\n\n    Ok(MontyObject::Dataclass {\n        name,\n        type_id,\n        field_names,\n        attrs: attrs.into(),\n        frozen,\n    })\n}\n\n/// Converts a `MontyObject::Dataclass` to a Python object.\n///\n/// If the `type_id` is found in the dc_registry, creates an instance of the original\n/// Python dataclass type (so `isinstance(result, OriginalClass)` works).\n/// Otherwise, falls back to creating a `PyUnknownDataclass`.\npub fn dataclass_to_py(\n    py: Python<'_>,\n    name: &str,\n    type_id: u64,\n    field_names: &[String],\n    attrs: &DictPairs,\n    frozen: bool,\n    dc_registry: &DcRegistry,\n) -> PyResult<Py<PyAny>> {\n    // Try to use the original type from the dc_registry (keyed by type_id)\n    if let Some(original_type_py) = dc_registry.get(py, type_id)? {\n        let original_type = original_type_py.bind(py).cast::<PyType>()?;\n        // Build kwargs dict from field names and values\n        let kwargs = PyDict::new(py);\n        for (key, value) in attrs {\n            // Skip non-string keys\n            if let MontyObject::String(s) = key {\n                // Only include declared fields in constructor kwargs\n                let key_str = s.as_str();\n                if field_names.iter().any(|f| f.as_str() == key_str) {\n                    kwargs.set_item(key_str, monty_to_py(py, value, dc_registry)?)?;\n                }\n            }\n        }\n\n        // Call the dataclass constructor with kwargs\n        original_type.call((), Some(&kwargs)).map(Bound::unbind)\n    } else {\n        // Fall back to PyUnknownDataclass\n        let dc = PyUnknownDataclass::new(py, name.to_string(), field_names.to_vec(), attrs, frozen, dc_registry)?;\n        Ok(Py::new(py, dc)?.into_any())\n    }\n}\n\n/// Maps Python dataclass type identity (pointer address as `u64`) to the original\n/// Python type object (`Py<PyAny>`).\n///\n/// This registry enables round-trip reconstruction of dataclass types: when a\n/// dataclass passes through Monty, its type is stored here so that on output,\n/// `isinstance(result, OriginalClass)` works correctly.\n///\n/// Wraps a `Py<PyDict>` so that `clone_ref` produces a shared handle to the same\n/// underlying dict — all clones see the same data without needing `Arc<Mutex>`.\n/// The GIL already serializes access, making additional locking unnecessary.\n#[derive(Debug)]\npub struct DcRegistry {\n    registry: Py<PyDict>,\n}\n\nimpl DcRegistry {\n    /// Creates a new empty registry.\n    pub fn new(py: Python<'_>) -> Self {\n        Self {\n            registry: PyDict::new(py).unbind(),\n        }\n    }\n\n    /// Creates a `DcRegistry` from an optional Python list of dataclass types.\n    ///\n    /// Each type in the list is registered by its pointer identity, matching the key\n    /// format used by `dataclass_to_monty`.\n    pub fn from_list(py: Python<'_>, dataclass_registry: Option<&Bound<'_, PyList>>) -> PyResult<Self> {\n        let slf = Self::new(py);\n\n        if let Some(registry_list) = dataclass_registry {\n            for cls in registry_list {\n                slf.insert(&cls)?;\n            }\n        }\n        Ok(slf)\n    }\n\n    /// Creates a shared handle to this registry (cheap Python refcount bump).\n    ///\n    /// The clone points to the **same** underlying Python dict, so insertions\n    /// through any handle are visible to all others.\n    pub fn clone_ref(&self, py: Python<'_>) -> Self {\n        Self {\n            registry: self.registry.clone_ref(py),\n        }\n    }\n\n    /// Registers a Python type in the dataclass registry, keyed by pointer identity.\n    ///\n    /// This is idempotent — calling it multiple times with the same type is safe and\n    /// simply overwrites the existing entry. The key is the raw pointer address of the\n    /// type object, matching what `dataclass_to_monty` stores as `type_id` in\n    /// `MontyObject::Dataclass`. This allows `dataclass_to_py` to look up the original\n    /// Python class when reconstructing output values.\n    pub fn insert<T>(&self, obj: &Bound<'_, T>) -> PyResult<()> {\n        let py = obj.py();\n        let type_id = obj.as_ptr() as u64;\n        self.registry.bind(py).set_item(type_id, obj.as_any())\n    }\n\n    /// Looks up an original Python type by its pointer identity.\n    pub fn get(&self, py: Python<'_>, type_id: u64) -> PyResult<Option<Py<PyAny>>> {\n        Ok(self.registry.bind(py).get_item(type_id)?.map(Bound::unbind))\n    }\n}\n\n/// Python class that mimics dataclass behavior for `MontyObject::Dataclass`.\n///\n/// Supports:\n/// - Attribute access (`__getattr__`, `__setattr__`)\n/// - String representation (`__repr__`, `__str__`)\n/// - Equality comparison (`__eq__`)\n/// - Hashing for frozen instances (`__hash__`)\n/// - `dataclasses` module compatibility (`__dataclass_fields__`)\n#[pyclass(name = \"UnknownDataclass\")]\npub struct PyUnknownDataclass {\n    /// Class name (e.g., \"Point\", \"User\")\n    name: String,\n    /// Declared field names in definition order (for repr)\n    field_names: Vec<String>,\n    /// All attributes (fields + any extra attrs)\n    attrs: Py<PyDict>,\n    /// Whether this instance is frozen (immutable)\n    frozen: bool,\n}\n\n#[pymethods]\nimpl PyUnknownDataclass {\n    /// Returns a dict mapping field names to Field objects.\n    ///\n    /// This enables compatibility with `dataclasses.is_dataclass()`, `dataclasses.fields()`,\n    /// `dataclasses.asdict()`, etc.\n    #[getter]\n    fn __dataclass_fields__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {\n        let field_marker = get_field_marker(py)?;\n        let missing = get_missing(py)?;\n        let field_class = get_field_class(py)?;\n        let attrs = self.attrs.bind(py);\n\n        let fields_dict = PyDict::new(py);\n        for field_name in &self.field_names {\n            // Get the field value's type for the type annotation\n            let field_type = if let Some(value) = attrs.get_item(field_name)? {\n                value.get_type().into_any()\n            } else {\n                py.None().into_bound(py).get_type().into_any()\n            };\n\n            // Create a Field object with the required attributes\n            let field_obj = if cfg!(Py_3_14) {\n                // Field(default, default_factory, init, repr, hash, compare, metadata, kw_only, doc)\n                // doc is now in 3.14\n                // https://github.com/python/cpython/blob/3.14/Lib/dataclasses.py#L294\n                field_class.call1((\n                    missing,   // default\n                    missing,   // default_factory\n                    true,      // init\n                    true,      // repr\n                    py.None(), // hash (None means use compare value)\n                    true,      // compare\n                    py.None(), // metadata\n                    false,     // kw_only\n                    py.None(), // doc\n                ))?\n            } else {\n                // https://github.com/python/cpython/blob/3.13/Lib/dataclasses.py#L288\n                // Field(default, default_factory, init, repr, hash, compare, metadata, kw_only)\n                field_class.call1((\n                    missing,   // default\n                    missing,   // default_factory\n                    true,      // init\n                    true,      // repr\n                    py.None(), // hash (None means use compare value)\n                    true,      // compare\n                    py.None(), // metadata\n                    false,     // kw_only\n                ))?\n            };\n\n            // Set name and type (these are set after construction in real dataclasses)\n            field_obj.setattr(\"name\", field_name)?;\n            field_obj.setattr(\"type\", field_type)?;\n            field_obj.setattr(\"_field_type\", field_marker)?;\n\n            fields_dict.set_item(field_name, field_obj)?;\n        }\n        Ok(fields_dict.unbind())\n    }\n\n    /// Returns a `_DataclassParams` object with dataclass configuration.\n    ///\n    /// This enables compatibility with code that checks `obj.__dataclass_params__.frozen`, etc.\n    #[getter]\n    fn __dataclass_params__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {\n        let params_class = get_dataclass_params_class(py)?;\n        let params = if cfg!(Py_3_12) {\n            // https://github.com/python/cpython/blob/3.12/Lib/dataclasses.py#L373\n            // _DataclassParams(init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot)\n            params_class.call1((\n                true,        // init\n                true,        // repr\n                true,        // eq\n                false,       // order\n                false,       // unsafe_hash\n                self.frozen, // frozen\n                true,        // match_args\n                false,       // kw_only\n                false,       // slots\n                false,       // weakref_slot\n            ))?\n        } else {\n            // https://github.com/python/cpython/blob/3.11/Lib/dataclasses.py#L346\n            // _DataclassParams(init, repr, eq, order, unsafe_hash, frozen)\n            params_class.call1((\n                true,        // init\n                true,        // repr\n                true,        // eq\n                false,       // order\n                false,       // unsafe_hash\n                self.frozen, // frozen\n            ))?\n        };\n        Ok(params.unbind())\n    }\n\n    /// Get an attribute value.\n    fn __getattr__(&self, py: Python<'_>, name: &str) -> PyResult<Py<PyAny>> {\n        let attrs = self.attrs.bind(py);\n        match attrs.get_item(name)? {\n            Some(value) => Ok(value.unbind()),\n            None => Err(PyAttributeError::new_err(format!(\n                \"'UnknownDataclass' object has no attribute '{name}'\",\n            ))),\n        }\n    }\n\n    /// Set an attribute value.\n    ///\n    /// Raises `FrozenInstanceError` (subclass of `AttributeError`) for frozen dataclasses.\n    fn __setattr__(&self, py: Python<'_>, name: &str, value: Py<PyAny>) -> PyResult<()> {\n        if self.frozen {\n            let frozen_error = get_frozen_instance_error(py)?;\n            let msg = format!(\"cannot assign to field '{name}'\");\n            return Err(PyErr::from_value(frozen_error.call1((msg,))?));\n        }\n        let attrs = self.attrs.bind(py);\n        attrs.set_item(name, value)?;\n        Ok(())\n    }\n\n    /// String representation: ClassName(field1=value1, field2=value2, ...)\n    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {\n        let attrs = self.attrs.bind(py);\n        let mut parts = Vec::new();\n        for field_name in &self.field_names {\n            if let Some(value) = attrs.get_item(field_name)? {\n                let value_repr: String = value.repr()?.extract()?;\n                parts.push(format!(\"{field_name}={value_repr}\"));\n            }\n        }\n        Ok(format!(\"<Unknown Dataclass {}({})>\", self.name, parts.join(\", \")))\n    }\n\n    /// Equality comparison.\n    fn __eq__(&self, py: Python<'_>, other: &Bound<'_, PyAny>) -> PyResult<bool> {\n        // Check if other is also a PyUnknownDataclass\n        if let Ok(other_dc) = other.extract::<PyRef<'_, Self>>() {\n            if self.name != other_dc.name {\n                return Ok(false);\n            }\n            let self_attrs = self.attrs.bind(py);\n            let other_attrs = other_dc.attrs.bind(py);\n            // Compare all attrs\n            self_attrs.eq(other_attrs)\n        } else {\n            Ok(false)\n        }\n    }\n\n    /// Hash (only for frozen dataclasses).\n    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {\n        if !self.frozen {\n            return Err(PyTypeError::new_err(\"unhashable type: 'UnknownDataclass'\"));\n        }\n\n        let mut hasher = DefaultHasher::new();\n\n        let attrs = self.attrs.bind(py);\n        for field_name in &self.field_names {\n            field_name.hash(&mut hasher);\n            if let Some(value) = attrs.get_item(field_name)? {\n                let value_hash: isize = value.hash()?;\n                value_hash.hash(&mut hasher);\n            }\n        }\n        // Python's hash returns a signed integer; reinterpret bits for large values\n        let hash_u64 = hasher.finish();\n        #[cfg(target_pointer_width = \"64\")]\n        let hash_isize = isize::from_ne_bytes(hash_u64.to_ne_bytes());\n        #[cfg(not(target_pointer_width = \"64\"))]\n        let hash_isize = {\n            // On 32-bit: truncate to lower 32 bits, then reinterpret as i32 -> isize\n            let hash_u32 = hash_u64 as u32;\n            i32::from_ne_bytes(hash_u32.to_ne_bytes()) as isize\n        };\n        Ok(hash_isize)\n    }\n}\n\nimpl PyUnknownDataclass {\n    /// Creates a new `PyUnknownDataclass` from `MontyObject` fields.\n    pub fn new<'a>(\n        py: Python<'_>,\n        name: String,\n        field_names: Vec<String>,\n        attrs: impl IntoIterator<Item = &'a (MontyObject, MontyObject)>,\n        frozen: bool,\n        dc_registry: &DcRegistry,\n    ) -> PyResult<Self> {\n        let dict = PyDict::new(py);\n        for (k, v) in attrs {\n            dict.set_item(monty_to_py(py, k, dc_registry)?, monty_to_py(py, v, dc_registry)?)?;\n        }\n        Ok(Self {\n            name,\n            field_names,\n            attrs: dict.unbind(),\n            frozen,\n        })\n    }\n}\n\n/// Cached import of `dataclasses._FIELD` marker.\n///\n/// Used to match the logic from `dataclasses.fields()`:\n/// `tuple(f for f in fields.values() if f._field_type is _FIELD)`\nfn get_field_marker(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static DC_FIELD_MARKER: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    DC_FIELD_MARKER.import(py, \"dataclasses\", \"_FIELD\")\n}\n\n/// Cached import of `dataclasses.MISSING` sentinel.\nfn get_missing(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static DC_MISSING: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    DC_MISSING.import(py, \"dataclasses\", \"MISSING\")\n}\n\n/// Cached import of `dataclasses.Field` class.\nfn get_field_class(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static DC_FIELD_CLASS: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    DC_FIELD_CLASS.import(py, \"dataclasses\", \"Field\")\n}\n\n/// Cached import of `dataclasses._DataclassParams` class.\nfn get_dataclass_params_class(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static DC_PARAMS_CLASS: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    DC_PARAMS_CLASS.import(py, \"dataclasses\", \"_DataclassParams\")\n}\n\n/// Cached import of `dataclasses.FrozenInstanceError` exception class.\npub fn get_frozen_instance_error(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static DC_FROZEN_ERROR: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    DC_FROZEN_ERROR.import(py, \"dataclasses\", \"FrozenInstanceError\")\n}\n"
  },
  {
    "path": "crates/monty-python/src/exceptions.rs",
    "content": "//! Custom exception types for the Monty Python interpreter.\n//!\n//! Provides a hierarchy of exception types that wrap Monty's internal exceptions,\n//! preserving traceback information and allowing Python code to distinguish\n//! between syntax errors, runtime errors, and type checking errors from Monty-executed code.\n//!\n//! ## Exception Hierarchy\n//!\n//! ```text\n//! MontyError(Exception)        # Base class for all Monty exceptions\n//! ├── MontySyntaxError         # Raised when syntax is invalid or Monty can't parse the code\n//! ├── MontyRuntimeError        # Raised when code fails during execution\n//! └── MontyTypingError         # Raised when type checking finds errors in the code\n//! ```\n\nuse ::monty::{ExcType, MontyException, StackFrame};\nuse monty_type_checking::TypeCheckingDiagnostics;\nuse pyo3::{\n    PyClassInitializer, PyTypeCheck,\n    exceptions::{self},\n    prelude::*,\n    sync::PyOnceLock,\n    types::{PyDict, PyList, PyString},\n};\n\nuse crate::dataclass::get_frozen_instance_error;\n\n/// Base exception for all Monty interpreter errors.\n///\n/// This is the parent class for both `MontySyntaxError` and `MontyRuntimeError`.\n/// Catching `MontyError` will catch any exception raised by Monty.\n#[pyclass(extends=exceptions::PyException, module=\"pydantic_monty\", subclass, skip_from_py_object)]\n#[derive(Clone)]\npub struct MontyError {\n    /// The underlying Monty exception.\n    exc: MontyException,\n}\n\nimpl MontyError {\n    /// Converts a Monty exception to a `PyErr`.\n    ///\n    /// For `SyntaxError` exceptions, creates a `MontySyntaxError`.\n    /// For all other exceptions, creates a `MontyRuntimeError` with all the exception\n    /// information preserved, including the traceback frames and display string.\n    #[must_use]\n    pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr {\n        // Syntax errors get their own exception type\n        if exc.exc_type() == ExcType::SyntaxError {\n            MontySyntaxError::new_err(py, exc)\n        } else {\n            MontyRuntimeError::new_err(py, exc)\n        }\n    }\n}\n\nimpl MontyError {\n    /// Creates a new `MontyError` wrapping a `MontyException`.\n    #[must_use]\n    pub fn new(exc: MontyException) -> Self {\n        Self { exc }\n    }\n\n    /// Returns the exception type.\n    fn exc_type(&self) -> ExcType {\n        self.exc.exc_type()\n    }\n\n    /// Returns the exception message, if any.\n    fn message(&self) -> Option<&str> {\n        self.exc.message()\n    }\n}\n\n#[pymethods]\nimpl MontyError {\n    /// Returns the inner exception as a Python exception object.\n    ///\n    /// This recreates a native Python exception (e.g., `ValueError`, `TypeError`)\n    /// from the stored exception type and message.\n    fn exception(&self, py: Python<'_>) -> Py<PyAny> {\n        let py_err = exc_monty_to_py(py, self.exc.clone());\n        py_err.into_value(py).into_any()\n    }\n\n    fn __str__(&self) -> String {\n        self.message().unwrap_or_default().to_string()\n    }\n\n    fn __repr__(&self) -> String {\n        let exc_type_name = self.exc_type();\n        if let Some(msg) = self.message() {\n            format!(\"MontyError({exc_type_name}: {msg})\")\n        } else {\n            format!(\"MontyError({exc_type_name})\")\n        }\n    }\n}\n\n/// Raised when Python code has syntax errors or cannot be parsed by Monty.\n///\n/// Inherits from `MontyError`. The inner exception is always a `SyntaxError`.\n#[pyclass(extends=MontyError, module=\"pydantic_monty\", skip_from_py_object)]\n#[derive(Clone)]\npub struct MontySyntaxError;\n\nimpl MontySyntaxError {\n    /// Creates a new `MontySyntaxError` with the given message.\n    #[must_use]\n    pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr {\n        let base_error = MontyError::new(exc);\n        let init = PyClassInitializer::from(base_error).add_subclass(Self);\n        match Py::new(py, init) {\n            Ok(err) => PyErr::from_value(err.into_bound(py).into_any()),\n            Err(e) => e,\n        }\n    }\n}\n\n#[pymethods]\nimpl MontySyntaxError {\n    /// Returns formatted exception string.\n    ///\n    /// Args:\n    ///     format: 'type-msg' - 'ExceptionType: message' format\n    ///             'msg' - just the message\n    #[pyo3(signature = (format = \"msg\"))]\n    #[expect(clippy::needless_pass_by_value, reason = \"required by macro\")]\n    fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult<String> {\n        let parent = slf.as_super();\n        match format {\n            \"msg\" => Ok(parent.message().unwrap_or_default().to_string()),\n            \"type-msg\" => Ok(parent.exc.summary()),\n            _ => Err(exceptions::PyValueError::new_err(format!(\n                \"Invalid display format: '{format}'. Expected 'type-msg', or 'msg'\"\n            ))),\n        }\n    }\n\n    #[expect(clippy::needless_pass_by_value, reason = \"required by macro\")]\n    fn __str__(slf: PyRef<'_, Self>) -> String {\n        slf.as_super().message().unwrap_or_default().to_string()\n    }\n\n    #[expect(clippy::needless_pass_by_value, reason = \"required by macro\")]\n    fn __repr__(slf: PyRef<'_, Self>) -> String {\n        let parent = slf.as_super();\n        if let Some(msg) = parent.message() {\n            format!(\"MontySyntaxError({msg})\")\n        } else {\n            \"MontySyntaxError()\".to_string()\n        }\n    }\n}\n\n/// Raised when type checking finds errors in the code.\n///\n/// Inherits from `MontyError`. This exception is raised when static type\n/// analysis detects type errors. Stores the `TypeCheckingFailure` so diagnostics\n/// can be re-rendered with different format/color settings via `display()`.\n#[pyclass(extends=MontyError, module=\"pydantic_monty\")]\npub struct MontyTypingError {\n    failure: TypeCheckingDiagnostics,\n}\n\nimpl MontyTypingError {\n    /// Creates a `MontyTypingError` from a `TypeCheckingFailure`.\n    #[must_use]\n    pub fn new_err(py: Python<'_>, failure: TypeCheckingDiagnostics) -> PyErr {\n        // we need a MontyException to create the base, but it shouldn't be visible anywhere\n        let base = MontyError::new(MontyException::new(ExcType::TypeError, None));\n        let init = PyClassInitializer::from(base).add_subclass(Self { failure });\n        match Py::new(py, init) {\n            Ok(err) => PyErr::from_value(err.into_bound(py).into_any()),\n            Err(e) => e,\n        }\n    }\n}\n\n#[pymethods]\nimpl MontyTypingError {\n    /// Renders the type error diagnostics with the specified format and color.\n    ///\n    /// Args:\n    ///     format: Output format\n    ///     color: Whether to include ANSI color codes in the output.\n    #[pyo3(signature = (format = \"full\", color = false))]\n    fn display(&self, format: &str, color: bool) -> PyResult<String> {\n        self.failure\n            .clone()\n            .color(color)\n            .format_from_str(format)\n            .map_err(exceptions::PyValueError::new_err)\n            .map(|f| f.to_string())\n    }\n\n    fn __str__(&self) -> String {\n        self.failure.to_string()\n    }\n\n    fn __repr__(&self) -> String {\n        format!(\"MontyTypingError({})\", self.failure)\n    }\n}\n\n/// Raised when Monty code fails during execution.\n///\n/// Inherits from `MontyError`. Additionally provides `traceback()` to access\n/// the Monty stack frames where the error occurred.\n#[pyclass(extends=MontyError, module=\"pydantic_monty\")]\npub struct MontyRuntimeError {\n    /// The traceback frames where the error occurred (pre-converted to Python objects).\n    frames: Vec<Py<PyFrame>>,\n}\n\nimpl MontyRuntimeError {\n    /// Creates a new `MontyRuntimeError` from the given exception data.\n    #[must_use]\n    pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr {\n        // Convert stack frames to PyFrame objects\n        let frames_result: PyResult<Vec<Py<PyFrame>>> = exc\n            .traceback()\n            .iter()\n            .map(|f| Py::new(py, PyFrame::from_stack_frame(f)))\n            .collect();\n\n        let frames = match frames_result {\n            Ok(frames) => frames,\n            Err(e) => return e,\n        };\n\n        let base_error = MontyError::new(exc);\n        // Create the MontyRuntimeError with proper initialization\n        let runtime_error = Self { frames };\n\n        let init = pyo3::PyClassInitializer::from(base_error).add_subclass(runtime_error);\n        match Py::new(py, init) {\n            Ok(err) => PyErr::from_value(err.into_bound(py).into_any()),\n            Err(e) => e,\n        }\n    }\n}\n\n#[pymethods]\nimpl MontyRuntimeError {\n    /// Returns the Monty traceback as a list of Frame objects.\n    fn traceback(&self, py: Python<'_>) -> Py<PyList> {\n        PyList::new(py, &self.frames)\n            .expect(\"failed to create frames list\")\n            .unbind()\n    }\n\n    /// Returns formatted exception string.\n    ///\n    /// Overrides the base class to provide the full traceback when format='traceback'.\n    #[pyo3(signature = (format = \"traceback\"))]\n    #[expect(clippy::needless_pass_by_value, reason = \"required by macro\")]\n    fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult<String> {\n        match format {\n            \"traceback\" => Ok(slf.as_super().exc.to_string()),\n            \"type-msg\" => Ok(slf.as_super().exc.summary()),\n            \"msg\" => Ok(slf.as_super().message().unwrap_or_default().to_string()),\n            _ => Err(exceptions::PyValueError::new_err(format!(\n                \"Invalid display format: '{format}'. Expected 'traceback', 'type-msg', or 'msg'\"\n            ))),\n        }\n    }\n\n    #[expect(clippy::needless_pass_by_value, reason = \"required by macro\")]\n    fn __str__(slf: PyRef<'_, Self>) -> String {\n        let parent = slf.as_super();\n        let exc_type_name = parent.exc_type();\n        if let Some(msg) = parent.message()\n            && !msg.is_empty()\n        {\n            return format!(\"{exc_type_name}: {msg}\");\n        }\n        format!(\"{exc_type_name}\")\n    }\n\n    #[expect(clippy::needless_pass_by_value, reason = \"required by macro\")]\n    fn __repr__(slf: PyRef<'_, Self>) -> String {\n        let parent = slf.as_super();\n        let exc_type_name = parent.exc_type();\n        if let Some(msg) = parent.message()\n            && !msg.is_empty()\n        {\n            return format!(\"MontyRuntimeError({exc_type_name}: {msg})\");\n        }\n        format!(\"MontyRuntimeError({exc_type_name})\")\n    }\n}\n\n/// A single frame in a Monty traceback.\n///\n/// Contains all the information needed to display a traceback line:\n/// the file location, function name, and optional source code preview.\n#[pyclass(name = \"Frame\", module = \"pydantic_monty\", frozen, skip_from_py_object)]\n#[derive(Debug, Clone)]\npub struct PyFrame {\n    /// The filename where the code is located.\n    #[pyo3(get)]\n    pub filename: String,\n    /// Line number (1-based).\n    #[pyo3(get)]\n    pub line: u16,\n    /// Column number (1-based).\n    #[pyo3(get)]\n    pub column: u16,\n    /// End line number (1-based).\n    #[pyo3(get)]\n    pub end_line: u16,\n    /// End column number (1-based).\n    #[pyo3(get)]\n    pub end_column: u16,\n    /// The name of the function, or None for module-level code.\n    #[pyo3(get)]\n    pub function_name: Option<String>,\n    /// The source code line for preview in the traceback.\n    #[pyo3(get)]\n    pub source_line: Option<String>,\n}\n\n#[pymethods]\nimpl PyFrame {\n    fn dict(&self, py: Python<'_>) -> Py<PyDict> {\n        let dict = PyDict::new(py);\n        dict.set_item(\"filename\", self.filename.clone()).unwrap();\n        dict.set_item(\"line\", self.line).unwrap();\n        dict.set_item(\"column\", self.column).unwrap();\n        dict.set_item(\"end_line\", self.end_line).unwrap();\n        dict.set_item(\"end_column\", self.end_column).unwrap();\n        dict.set_item(\"function_name\", self.function_name.clone()).unwrap();\n        dict.set_item(\"source_line\", self.source_line.clone()).unwrap();\n        dict.unbind()\n    }\n\n    fn __repr__(&self) -> String {\n        let func = self.function_name.as_ref().map_or(\"<module>\".to_string(), Clone::clone);\n        format!(\n            \"Frame(filename='{}', line={}, column={}, function_name='{}')\",\n            self.filename, self.line, self.column, func\n        )\n    }\n}\n\nimpl PyFrame {\n    /// Creates a `PyFrame` from Monty's `StackFrame`.\n    #[must_use]\n    pub fn from_stack_frame(frame: &StackFrame) -> Self {\n        Self {\n            filename: frame.filename.clone(),\n            line: frame.start.line,\n            column: frame.start.column,\n            end_line: frame.end.line,\n            end_column: frame.end.column,\n            function_name: frame.frame_name.clone(),\n            source_line: frame.preview_line.clone(),\n        }\n    }\n}\n\n/// Converts Monty's `MontyException` to the matching Python exception value.\n///\n/// Creates an appropriate Python exception type with the message.\n/// The traceback information is included in the exception message\n/// since PyO3 doesn't provide direct traceback manipulation.\npub fn exc_monty_to_py(py: Python<'_>, exc: MontyException) -> PyErr {\n    let exc_type = exc.exc_type();\n    let msg = exc.into_message().unwrap_or_default();\n\n    match exc_type {\n        ExcType::Exception => exceptions::PyException::new_err(msg),\n        ExcType::BaseException => exceptions::PyBaseException::new_err(msg),\n        ExcType::SystemExit => exceptions::PySystemExit::new_err(msg),\n        ExcType::KeyboardInterrupt => exceptions::PyKeyboardInterrupt::new_err(msg),\n        ExcType::ArithmeticError => exceptions::PyArithmeticError::new_err(msg),\n        ExcType::OverflowError => exceptions::PyOverflowError::new_err(msg),\n        ExcType::ZeroDivisionError => exceptions::PyZeroDivisionError::new_err(msg),\n        ExcType::LookupError => exceptions::PyLookupError::new_err(msg),\n        ExcType::IndexError => exceptions::PyIndexError::new_err(msg),\n        ExcType::KeyError => exceptions::PyKeyError::new_err(msg),\n        ExcType::RuntimeError => exceptions::PyRuntimeError::new_err(msg),\n        ExcType::NotImplementedError => exceptions::PyNotImplementedError::new_err(msg),\n        ExcType::RecursionError => exceptions::PyRecursionError::new_err(msg),\n        ExcType::AssertionError => exceptions::PyAssertionError::new_err(msg),\n        ExcType::AttributeError => exceptions::PyAttributeError::new_err(msg),\n        ExcType::FrozenInstanceError => {\n            if let Ok(exc_cls) = get_frozen_instance_error(py)\n                && let Ok(exc_instance) = exc_cls.call1((PyString::new(py, &msg),))\n            {\n                return PyErr::from_value(exc_instance);\n            }\n            // if creating the right exception fails, fallback to AttributeError which it's a subclass of\n            exceptions::PyAttributeError::new_err(msg)\n        }\n        ExcType::MemoryError => exceptions::PyMemoryError::new_err(msg),\n        ExcType::NameError => exceptions::PyNameError::new_err(msg),\n        ExcType::UnboundLocalError => exceptions::PyUnboundLocalError::new_err(msg),\n        ExcType::StopIteration => exceptions::PyStopIteration::new_err(msg),\n        ExcType::SyntaxError => exceptions::PySyntaxError::new_err(msg),\n        ExcType::TimeoutError => exceptions::PyTimeoutError::new_err(msg),\n        ExcType::TypeError => exceptions::PyTypeError::new_err(msg),\n        ExcType::ValueError => exceptions::PyValueError::new_err(msg),\n        ExcType::UnicodeDecodeError => exceptions::PyUnicodeDecodeError::new_err(msg),\n        ExcType::ImportError => exceptions::PyImportError::new_err(msg),\n        ExcType::ModuleNotFoundError => exceptions::PyModuleNotFoundError::new_err(msg),\n        ExcType::OSError => exceptions::PyOSError::new_err(msg),\n        ExcType::FileNotFoundError => exceptions::PyFileNotFoundError::new_err(msg),\n        ExcType::FileExistsError => exceptions::PyFileExistsError::new_err(msg),\n        ExcType::IsADirectoryError => exceptions::PyIsADirectoryError::new_err(msg),\n        ExcType::NotADirectoryError => exceptions::PyNotADirectoryError::new_err(msg),\n        ExcType::RePatternError => {\n            if let Ok(re_pattern_error) = get_re_pattern_error(py)\n                && let Ok(exc_instance) = re_pattern_error.call1((PyString::new(py, &msg),))\n            {\n                PyErr::from_value(exc_instance)\n            } else {\n                exceptions::PyRuntimeError::new_err(msg)\n            }\n        }\n    }\n}\n\n/// Converts a python exception to monty.\n///\n/// Used when resuming execution with an exception from Python.\npub fn exc_py_to_monty(py: Python<'_>, py_err: &PyErr) -> MontyException {\n    let exc = py_err.value(py);\n    let exc_type = py_err_to_exc_type(exc);\n    let arg = exc.str().ok().map(|s| s.to_string_lossy().into_owned());\n\n    MontyException::new(exc_type, arg)\n}\n\n/// Converts a Python exception to Monty's `MontyObject::Exception`.\npub fn exc_to_monty_object(exc: &Bound<'_, exceptions::PyBaseException>) -> ::monty::MontyObject {\n    let exc_type = py_err_to_exc_type(exc);\n    let arg = exc.str().ok().map(|s| s.to_string_lossy().into_owned());\n\n    ::monty::MontyObject::Exception { exc_type, arg }\n}\n\n/// Maps a Python exception type to Monty's `ExcType` enum.\n///\n/// NOTE: order matters here as some exceptions are subclasses of others!\n/// In general we group exceptions by their type hierarchy to improve performance.\nfn py_err_to_exc_type(exc: &Bound<'_, exceptions::PyBaseException>) -> ExcType {\n    // Exception hierarchy\n    if exceptions::PyException::type_check(exc) {\n        // put the most commonly used exceptions first\n        if exceptions::PyTypeError::type_check(exc) {\n            ExcType::TypeError\n        // ValueError hierarchy (check UnicodeDecodeError first as it's a subclass)\n        } else if exceptions::PyValueError::type_check(exc) {\n            if exceptions::PyUnicodeDecodeError::type_check(exc) {\n                ExcType::UnicodeDecodeError\n            } else {\n                ExcType::ValueError\n            }\n        } else if exceptions::PyAssertionError::type_check(exc) {\n            ExcType::AssertionError\n        } else if exceptions::PySyntaxError::type_check(exc) {\n            ExcType::SyntaxError\n        // LookupError hierarchy\n        } else if exceptions::PyLookupError::type_check(exc) {\n            if exceptions::PyKeyError::type_check(exc) {\n                ExcType::KeyError\n            } else if exceptions::PyIndexError::type_check(exc) {\n                ExcType::IndexError\n            } else {\n                ExcType::LookupError\n            }\n        // ArithmeticError hierarchy\n        } else if exceptions::PyArithmeticError::type_check(exc) {\n            if exceptions::PyZeroDivisionError::type_check(exc) {\n                ExcType::ZeroDivisionError\n            } else if exceptions::PyOverflowError::type_check(exc) {\n                ExcType::OverflowError\n            } else {\n                ExcType::ArithmeticError\n            }\n        // RuntimeError hierarchy\n        } else if exceptions::PyRuntimeError::type_check(exc) {\n            if exceptions::PyNotImplementedError::type_check(exc) {\n                ExcType::NotImplementedError\n            } else if exceptions::PyRecursionError::type_check(exc) {\n                ExcType::RecursionError\n            } else {\n                ExcType::RuntimeError\n            }\n        // AttributeError hierarchy\n        } else if exceptions::PyAttributeError::type_check(exc) {\n            if is_frozen_instance_error(exc) {\n                ExcType::FrozenInstanceError\n            } else {\n                ExcType::AttributeError\n            }\n        // NameError hierarchy (check UnboundLocalError first as it's a subclass)\n        } else if exceptions::PyNameError::type_check(exc) {\n            if exceptions::PyUnboundLocalError::type_check(exc) {\n                ExcType::UnboundLocalError\n            } else {\n                ExcType::NameError\n            }\n        // OSError hierarchy (check specific subclasses first)\n        } else if exceptions::PyOSError::type_check(exc) {\n            if exceptions::PyFileNotFoundError::type_check(exc) {\n                ExcType::FileNotFoundError\n            } else if exceptions::PyFileExistsError::type_check(exc) {\n                ExcType::FileExistsError\n            } else if exceptions::PyIsADirectoryError::type_check(exc) {\n                ExcType::IsADirectoryError\n            } else if exceptions::PyNotADirectoryError::type_check(exc) {\n                ExcType::NotADirectoryError\n            } else {\n                ExcType::OSError\n            }\n        // other standalone exception types\n        } else if exceptions::PyTimeoutError::type_check(exc) {\n            ExcType::TimeoutError\n        } else if exceptions::PyMemoryError::type_check(exc) {\n            ExcType::MemoryError\n        } else {\n            ExcType::Exception\n        }\n    // BaseException direct subclasses\n    } else if exceptions::PySystemExit::type_check(exc) {\n        ExcType::SystemExit\n    } else if exceptions::PyKeyboardInterrupt::type_check(exc) {\n        ExcType::KeyboardInterrupt\n    // Catch-all for BaseException\n    } else {\n        ExcType::BaseException\n    }\n}\n\n/// Checks if an exception is an instance of `dataclasses.FrozenInstanceError`.\n///\n/// Since `FrozenInstanceError` is not a built-in PyO3 exception type, we need to\n/// check using Python's isinstance against the imported class.\nfn is_frozen_instance_error(exc: &Bound<'_, exceptions::PyBaseException>) -> bool {\n    if let Ok(frozen_error_cls) = get_frozen_instance_error(exc.py()) {\n        exc.is_instance(frozen_error_cls).unwrap_or(false)\n    } else {\n        false\n    }\n}\n\nfn get_re_pattern_error(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {\n    static RE_PATTERN_ERROR: PyOnceLock<Py<PyAny>> = PyOnceLock::new();\n\n    if cfg!(Py_3_13) {\n        RE_PATTERN_ERROR.import(py, \"re\", \"PatternError\")\n    } else {\n        RE_PATTERN_ERROR.import(py, \"re\", \"error\")\n    }\n}\n"
  },
  {
    "path": "crates/monty-python/src/external.rs",
    "content": "//! External function callback support.\n//!\n//! Allows Python code running in Monty to call back to host Python functions.\n//! External functions are registered by name and called when Monty execution\n//! reaches a call to that function.\n\nuse ::monty::{ExtFunctionResult, MontyObject};\nuse pyo3::{\n    exceptions::PyRuntimeError,\n    prelude::*,\n    types::{PyDict, PyTuple},\n};\n\nuse crate::{\n    convert::{monty_to_py, py_to_monty},\n    dataclass::DcRegistry,\n    exceptions::exc_py_to_monty,\n};\n\n/// Dispatches a dataclass method call back to the original Python object.\n///\n/// When Monty encounters a call like `dc.my_method(args)`, the VM pauses with a\n/// `FrameExit::MethodCall` containing the method name (e.g. `\"my_method\"`)\n/// and the dataclass instance as the first arg. This function:\n/// 1. Converts the first arg (dataclass `self`) back to a Python object\n/// 2. Calls `getattr(self_obj, method_name)(*remaining_args, **kwargs)`\n/// 3. Converts the result back to Monty format\npub fn dispatch_method_call(\n    py: Python<'_>,\n    function_name: &str,\n    args: &[MontyObject],\n    kwargs: &[(MontyObject, MontyObject)],\n    dc_registry: &DcRegistry,\n) -> ExtFunctionResult {\n    match dispatch_method_call_inner(py, function_name, args, kwargs, dc_registry) {\n        Ok(result) => ExtFunctionResult::Return(result),\n        Err(err) => ExtFunctionResult::Error(exc_py_to_monty(py, &err)),\n    }\n}\n\n/// Inner implementation of method dispatch that returns `PyResult` for error handling.\nfn dispatch_method_call_inner(\n    py: Python<'_>,\n    function_name: &str,\n    args: &[MontyObject],\n    kwargs: &[(MontyObject, MontyObject)],\n    dc_registry: &DcRegistry,\n) -> PyResult<MontyObject> {\n    // First arg is the dataclass self\n    let mut args_iter = args.iter();\n    let self_obj = args_iter\n        .next()\n        .ok_or_else(|| PyRuntimeError::new_err(\"Method call missing self argument\"))?;\n    let py_self = monty_to_py(py, self_obj, dc_registry)?;\n\n    // Get the method from the object\n    let method = py_self.bind(py).getattr(function_name)?;\n\n    let result = if args.len() == 1 && kwargs.is_empty() {\n        method.call0()?\n    } else {\n        // Convert remaining positional arguments\n        let remaining_args: PyResult<Vec<Py<PyAny>>> = args_iter.map(|arg| monty_to_py(py, arg, dc_registry)).collect();\n        let py_args_tuple = PyTuple::new(py, remaining_args?)?;\n\n        // Call the method\n        let py_kwargs = if kwargs.is_empty() {\n            None\n        } else {\n            // Convert keyword arguments\n            let py_kwargs = PyDict::new(py);\n            for (key, value) in kwargs {\n                let py_key = monty_to_py(py, key, dc_registry)?;\n                let py_value = monty_to_py(py, value, dc_registry)?;\n                py_kwargs.set_item(py_key, py_value)?;\n            }\n            Some(py_kwargs)\n        };\n        method.call(&py_args_tuple, py_kwargs.as_ref())?\n    };\n\n    py_to_monty(&result, dc_registry)\n}\n\n/// Registry that maps external function names to Python callables.\n///\n/// Passed to the execution loop and used to dispatch calls when Monty\n/// execution pauses at an external function. The `dc_registry` is a\n/// GIL-protected `PyDict` wrapper, so auto-registration of dataclass types\n/// encountered in return values is transparent to callers.\npub struct ExternalFunctionRegistry<'a, 'py> {\n    py: Python<'py>,\n    functions: &'py Bound<'py, PyDict>,\n    dc_registry: &'a DcRegistry,\n}\n\nimpl<'a, 'py> ExternalFunctionRegistry<'a, 'py> {\n    /// Creates a new registry from a Python dict of `name -> callable`.\n    pub fn new(py: Python<'py>, functions: &'py Bound<'py, PyDict>, dc_registry: &'a DcRegistry) -> Self {\n        Self {\n            py,\n            functions,\n            dc_registry,\n        }\n    }\n\n    /// Calls an external function by name with Monty arguments.\n    ///\n    /// Converts args/kwargs from Monty format, calls the Python callable\n    /// with unpacked `*args, **kwargs`, and converts the result back to Monty format.\n    ///\n    /// If the Python function raises an exception, it's converted to a Monty\n    /// exception that will be raised inside Monty execution.\n    pub fn call(\n        &self,\n        function_name: &str,\n        args: &[MontyObject],\n        kwargs: &[(MontyObject, MontyObject)],\n    ) -> ExtFunctionResult {\n        match self.call_inner(function_name, args, kwargs) {\n            Ok(Some(result)) => ExtFunctionResult::Return(result),\n            Ok(None) => ExtFunctionResult::NotFound(function_name.to_owned()),\n            Err(err) => ExtFunctionResult::Error(exc_py_to_monty(self.py, &err)),\n        }\n    }\n\n    /// Inner implementation that returns `PyResult` for error handling.\n    fn call_inner(\n        &self,\n        function_name: &str,\n        args: &[MontyObject],\n        kwargs: &[(MontyObject, MontyObject)],\n    ) -> PyResult<Option<MontyObject>> {\n        // Look up the callable\n        let Some(callable) = self.functions.get_item(function_name)? else {\n            return Ok(None);\n        };\n\n        // Convert positional arguments to Python objects\n        let py_args: PyResult<Vec<Py<PyAny>>> = args\n            .iter()\n            .map(|arg| monty_to_py(self.py, arg, self.dc_registry))\n            .collect();\n        let py_args_tuple = PyTuple::new(self.py, py_args?)?;\n\n        // Convert keyword arguments to Python dict\n        let py_kwargs = PyDict::new(self.py);\n        for (key, value) in kwargs {\n            // Keys in kwargs should be strings\n            let py_key = monty_to_py(self.py, key, self.dc_registry)?;\n            let py_value = monty_to_py(self.py, value, self.dc_registry)?;\n            py_kwargs.set_item(py_key, py_value)?;\n        }\n\n        // Call the function with unpacked *args, **kwargs\n        let result = if py_kwargs.is_empty() {\n            callable.call1(&py_args_tuple)?\n        } else {\n            callable.call(&py_args_tuple, Some(&py_kwargs))?\n        };\n\n        // Convert result back to Monty format\n        py_to_monty(&result, self.dc_registry).map(Some)\n    }\n}\n"
  },
  {
    "path": "crates/monty-python/src/lib.rs",
    "content": "//! Python bindings for the Monty sandboxed Python interpreter.\n//!\n//! This module provides a Python interface to Monty, allowing execution of\n//! sandboxed Python code with configurable resource limits and external\n//! function callbacks.\n\nmod convert;\nmod dataclass;\nmod exceptions;\nmod external;\nmod limits;\nmod monty_cls;\nmod repl;\nmod serialization;\n\nuse std::sync::OnceLock;\n\n// Use `::monty` to refer to the external crate (not the pymodule)\npub use exceptions::{MontyError, MontyRuntimeError, MontySyntaxError, MontyTypingError, PyFrame};\npub use monty_cls::{PyFunctionSnapshot, PyFutureSnapshot, PyMonty, PyMontyComplete, PyNameLookupSnapshot};\nuse pyo3::prelude::*;\npub use repl::PyMontyRepl;\n\n/// Copied from `get_pydantic_core_version` in pydantic\nfn get_version() -> &'static str {\n    static VERSION: OnceLock<String> = OnceLock::new();\n\n    VERSION.get_or_init(|| {\n        let version = env!(\"CARGO_PKG_VERSION\");\n        // cargo uses \"1.0-alpha1\" etc. while python uses \"1.0.0a1\", this is not full compatibility,\n        // but it's good enough for now\n        // see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec\n        // see https://peps.python.org/pep-0440/ for python spec\n        // it seems the dot after \"alpha/beta\" e.g. \"-alpha.1\" is not necessary, hence why this works\n        version.replace(\"-alpha\", \"a\").replace(\"-beta\", \"b\")\n    })\n}\n\n/// Monty - A sandboxed Python interpreter written in Rust.\n#[pymodule]\nmod _monty {\n    use pyo3::prelude::*;\n\n    #[pymodule_export]\n    use super::MontyError;\n    #[pymodule_export]\n    use super::MontyRuntimeError;\n    #[pymodule_export]\n    use super::MontySyntaxError;\n    #[pymodule_export]\n    use super::MontyTypingError;\n    #[pymodule_export]\n    use super::PyFrame as Frame;\n    #[pymodule_export]\n    use super::PyFunctionSnapshot as FunctionSnapshot;\n    #[pymodule_export]\n    use super::PyFutureSnapshot as FutureSnapshot;\n    #[pymodule_export]\n    use super::PyMonty as Monty;\n    #[pymodule_export]\n    use super::PyMontyComplete as MontyComplete;\n    #[pymodule_export]\n    use super::PyMontyRepl as MontyRepl;\n    #[pymodule_export]\n    use super::PyNameLookupSnapshot as NameLookupSnapshot;\n    use super::get_version;\n    #[pymodule_export]\n    use super::serialization::load_repl_snapshot;\n    #[pymodule_export]\n    use super::serialization::load_snapshot;\n\n    #[pymodule_init]\n    fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {\n        m.add(\"__version__\", get_version())?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/monty-python/src/limits.rs",
    "content": "//! Python wrapper for Monty's `ResourceLimits`.\n//!\n//! Provides a TypedDict interface to configure resource limits for code execution,\n//! including time limits, memory limits, and recursion depth.\n\nuse std::{\n    sync::atomic::{AtomicU16, Ordering},\n    time::Duration,\n};\n\nuse monty::{DEFAULT_MAX_RECURSION_DEPTH, ResourceError, ResourceTracker};\nuse pyo3::{prelude::*, types::PyDict};\n\nuse crate::exceptions::exc_py_to_monty;\n\n/// Extracts resource limits from a Python dict.\n///\n/// The dict should have the following optional keys:\n/// - `max_allocations`: Maximum number of heap allocations allowed (int)\n/// - `max_duration_secs`: Maximum execution time in seconds (float)\n/// - `max_memory`: Maximum heap memory in bytes (int)\n/// - `gc_interval`: Run garbage collection every N allocations (int)\n/// - `max_recursion_depth`: Maximum function call stack depth (int, default: 1000)\n///\n/// If a key is missing or set to `None`, that limit is not applied\n/// (except `max_recursion_depth` which defaults to 1000).\n///\n/// Raises `TypeError` if a value is present but has the wrong type.\npub fn extract_limits(dict: &Bound<'_, PyDict>) -> PyResult<monty::ResourceLimits> {\n    let max_allocations = extract_optional_usize(dict, \"max_allocations\")?;\n    let max_duration_secs = extract_optional_f64(dict, \"max_duration_secs\")?;\n    let max_memory = extract_optional_usize(dict, \"max_memory\")?;\n    let gc_interval = extract_optional_usize(dict, \"gc_interval\")?;\n    let max_recursion_depth =\n        extract_optional_usize(dict, \"max_recursion_depth\")?.or(Some(DEFAULT_MAX_RECURSION_DEPTH));\n\n    let mut limits = monty::ResourceLimits::new().max_recursion_depth(max_recursion_depth);\n\n    if let Some(max) = max_allocations {\n        limits = limits.max_allocations(max);\n    }\n    if let Some(secs) = max_duration_secs {\n        limits = limits.max_duration(Duration::from_secs_f64(secs));\n    }\n    if let Some(max) = max_memory {\n        limits = limits.max_memory(max);\n    }\n    if let Some(interval) = gc_interval {\n        limits = limits.gc_interval(interval);\n    }\n\n    Ok(limits)\n}\n\n/// Extracts an optional usize from a dict, raising `TypeError` if the value has the wrong type.\nfn extract_optional_usize(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<usize>> {\n    match dict.get_item(key)? {\n        None => Ok(None),\n        Some(value) if value.is_none() => Ok(None),\n        Some(value) => Ok(Some(value.extract()?)),\n    }\n}\n\n/// Extracts an optional f64 from a dict, raising `TypeError` if the value has the wrong type.\nfn extract_optional_f64(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<f64>> {\n    match dict.get_item(key)? {\n        None => Ok(None),\n        Some(value) if value.is_none() => Ok(None),\n        Some(value) => Ok(Some(value.extract()?)),\n    }\n}\n\n/// How often to check Python signals (every N calls to `check_time`).\n///\n/// This balances responsiveness to Ctrl+C against performance overhead.\n/// With ~1000 checks, signal handling adds negligible overhead while still\n/// responding to interrupts within a reasonable timeframe.\nconst SIGNAL_CHECK_INTERVAL: u16 = 1000;\n\n/// A resource tracker that wraps another ResourceTracker and periodically checks Python signals.\n///\n/// This allows Ctrl+C and other Python signals to interrupt long-running code\n/// executed through the monty interpreter. Signals are checked every\n/// `SIGNAL_CHECK_INTERVAL` calls to `check_time` (at statement boundaries).\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub struct PySignalTracker<T: ResourceTracker> {\n    inner: T,\n    /// Counter for check_time calls, used to rate-limit signal checks.\n    ///\n    /// Uses `AtomicU16` for interior mutability so `check_time` can take `&self`\n    /// (required by the `ResourceTracker` trait) while remaining `Sync` for PyO3.\n    check_counter: AtomicU16,\n}\n\nimpl<T: ResourceTracker> PySignalTracker<T> {\n    /// Creates a new signal-checking tracker wrapping the given tracker.\n    pub fn new(inner: T) -> Self {\n        Self {\n            inner,\n            check_counter: AtomicU16::new(0),\n        }\n    }\n\n    fn check_python_signals(&self) -> Result<(), ResourceError> {\n        // Periodically check Python signals\n        let count = self.check_counter.fetch_add(1, Ordering::Relaxed).wrapping_add(1);\n\n        if count.is_multiple_of(SIGNAL_CHECK_INTERVAL) {\n            Python::attach(|py| {\n                py.check_signals()\n                    .map_err(|e| ResourceError::Exception(exc_py_to_monty(py, &e)))\n            })?;\n        }\n        Ok(())\n    }\n}\n\nimpl<T: ResourceTracker> ResourceTracker for PySignalTracker<T> {\n    fn on_allocate(&mut self, get_size: impl FnOnce() -> usize) -> Result<(), ResourceError> {\n        self.inner.on_allocate(get_size)\n    }\n\n    fn on_free(&mut self, get_size: impl FnOnce() -> usize) {\n        self.inner.on_free(get_size);\n    }\n\n    fn check_time(&self) -> Result<(), ResourceError> {\n        // First check inner tracker's time limit\n        self.inner.check_time()?;\n\n        // then periodically check for Python signals\n        self.check_python_signals()\n    }\n\n    fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError> {\n        self.inner.check_recursion_depth(current_depth)\n    }\n\n    fn check_large_result(&self, estimated_bytes: usize) -> Result<(), ResourceError> {\n        self.inner.check_large_result(estimated_bytes)\n    }\n}\n"
  },
  {
    "path": "crates/monty-python/src/monty_cls.rs",
    "content": "use std::{\n    borrow::Cow,\n    fmt::Write,\n    sync::{Mutex, PoisonError},\n};\n\n// Use `::monty` to refer to the external crate (not the pymodule)\nuse ::monty::{\n    ExtFunctionResult, FunctionCall, LimitedTracker, MontyException, MontyObject, MontyRun, NameLookupResult,\n    NoLimitTracker, OsCall, PrintWriter, PrintWriterCallback, ReplFunctionCall, ReplNameLookup, ReplOsCall,\n    ReplProgress, ReplResolveFutures, ReplStartError, ResolveFutures, ResourceTracker, RunProgress,\n};\nuse monty::{ExcType, NameLookup};\nuse monty_type_checking::{SourceFile, type_check};\nuse pyo3::{\n    IntoPyObjectExt,\n    exceptions::{PyKeyError, PyRuntimeError, PyTypeError, PyValueError},\n    intern,\n    prelude::*,\n    types::{PyBytes, PyDict, PyList, PyTuple, PyType},\n};\nuse send_wrapper::SendWrapper;\n\nuse crate::{\n    convert::{get_docstring, monty_to_py, py_to_monty},\n    dataclass::DcRegistry,\n    exceptions::{MontyError, MontyTypingError, exc_py_to_monty},\n    external::{ExternalFunctionRegistry, dispatch_method_call},\n    limits::{PySignalTracker, extract_limits},\n    repl::{EitherRepl, FromCoreRepl, PyMontyRepl},\n};\n\n/// A sandboxed Python interpreter instance.\n///\n/// Parses and compiles Python code on initialization, then can be run\n/// multiple times with different input values. This separates the parsing\n/// cost from execution, making repeated runs more efficient.\n#[pyclass(name = \"Monty\", module = \"pydantic_monty\")]\n#[derive(Debug)]\npub struct PyMonty {\n    /// The compiled code snapshot, ready to execute.\n    runner: MontyRun,\n    /// The artificial name of the python code \"file\"\n    script_name: String,\n    /// Names of input variables expected by the code.\n    input_names: Vec<String>,\n    /// Registry of dataclass types for reconstructing original types on output.\n    ///\n    /// Maps type pointer identity (`u64`) to the original Python type, allowing\n    /// `isinstance(result, OriginalClass)` to work correctly after round-tripping through Monty.\n    dc_registry: DcRegistry,\n}\n\n#[pymethods]\nimpl PyMonty {\n    /// Creates a new Monty interpreter by parsing the given code.\n    ///\n    /// # Arguments\n    /// * `code` - Python code to execute\n    /// * `inputs` - List of input variable names available in the code\n    /// * `type_check` - Whether to perform type checking on the code\n    /// * `type_check_stubs` - Prefix code to be executed before type checking\n    /// * `dataclass_registry` - Registry of dataclass types for reconstructing original types on output.\n    #[new]\n    #[pyo3(signature = (code, *, script_name=\"main.py\", inputs=None, type_check=false, type_check_stubs=None, dataclass_registry=None))]\n    fn new(\n        py: Python<'_>,\n        code: String,\n        script_name: &str,\n        inputs: Option<&Bound<'_, PyList>>,\n        type_check: bool,\n        type_check_stubs: Option<&str>,\n        dataclass_registry: Option<&Bound<'_, PyList>>,\n    ) -> PyResult<Self> {\n        let input_names = list_str(inputs, \"inputs\")?;\n\n        if type_check {\n            py_type_check(py, &code, script_name, type_check_stubs)?;\n        }\n\n        // Create the snapshot (parses the code)\n        let runner = MontyRun::new(code, script_name, input_names.clone()).map_err(|e| MontyError::new_err(py, e))?;\n\n        Ok(Self {\n            runner,\n            script_name: script_name.to_string(),\n            input_names,\n            dc_registry: DcRegistry::from_list(py, dataclass_registry)?,\n        })\n    }\n\n    /// Registers a dataclass type for proper isinstance() support on output.\n    ///\n    /// When a dataclass passes through Monty and is returned, it becomes a `MontyDataclass`.\n    /// By registering the original type, `isinstance(result, OriginalClass)` will return `True`.\n    ///\n    /// # Arguments\n    /// * `cls` - The dataclass type to register\n    ///\n    /// # Raises\n    /// * `TypeError` if the argument is not a dataclass type\n    fn register_dataclass(&self, cls: &Bound<'_, PyType>) -> PyResult<()> {\n        self.dc_registry.insert(cls)\n    }\n\n    /// Performs static type checking on the code.\n    ///\n    /// Analyzes the code for type errors without executing it. This uses\n    /// a subset of Python's type system supported by Monty.\n    ///\n    /// # Args\n    /// * `prefix_code` - Optional prefix to prepend to the code before type checking,\n    ///   e.g. with inputs and external function signatures\n    ///\n    /// # Raises\n    /// * `RuntimeError` if type checking infrastructure fails\n    /// * `MontyTypingError` if type errors are found\n    #[pyo3(signature = (prefix_code=None))]\n    fn type_check(&self, py: Python<'_>, prefix_code: Option<&str>) -> PyResult<()> {\n        py_type_check(py, self.runner.code(), &self.script_name, prefix_code)\n    }\n\n    /// Executes the code and returns the result.\n    ///\n    /// # Returns\n    /// The result of the last expression in the code\n    ///\n    /// # Raises\n    /// Various Python exceptions matching what the code would raise\n    #[pyo3(signature = (*, inputs=None, limits=None, external_functions=None, print_callback=None, os=None))]\n    fn run(\n        &self,\n        py: Python<'_>,\n        inputs: Option<&Bound<'_, PyDict>>,\n        limits: Option<&Bound<'_, PyDict>>,\n        external_functions: Option<&Bound<'_, PyDict>>,\n        print_callback: Option<&Bound<'_, PyAny>>,\n        os: Option<&Bound<'_, PyAny>>,\n    ) -> PyResult<Py<PyAny>> {\n        // Clone the Arc handle — all clones share the same underlying registry,\n        // so auto-registrations during execution are visible to all users.\n        let input_values = self.extract_input_values(inputs, &self.dc_registry)?;\n\n        if let Some(os_callback) = os\n            && !os_callback.is_callable()\n        {\n            let msg = format!(\"TypeError: '{}' object is not callable\", os_callback.get_type().name()?);\n            return Err(PyTypeError::new_err(msg));\n        }\n\n        // Build print writer\n        let mut print_cb;\n        let print_writer = match print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::new(cb);\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n\n        // Run with appropriate tracker type (must branch due to different generic types)\n        if let Some(limits) = limits {\n            let tracker = PySignalTracker::new(LimitedTracker::new(extract_limits(limits)?));\n            self.run_impl(py, input_values, tracker, external_functions, os, print_writer)\n        } else {\n            let tracker = PySignalTracker::new(NoLimitTracker);\n            self.run_impl(py, input_values, tracker, external_functions, os, print_writer)\n        }\n    }\n\n    #[pyo3(signature = (*, inputs=None, limits=None, print_callback=None))]\n    fn start<'py>(\n        &self,\n        py: Python<'py>,\n        inputs: Option<&Bound<'py, PyDict>>,\n        limits: Option<&Bound<'py, PyDict>>,\n        print_callback: Option<Bound<'_, PyAny>>,\n    ) -> PyResult<Bound<'py, PyAny>> {\n        // Clone the Arc handle — shares the same underlying registry\n        let dc_registry = self.dc_registry.clone_ref(py);\n        let input_values = self.extract_input_values(inputs, &dc_registry)?;\n\n        // Build print writer - CallbackStringPrint is Send so GIL can be released\n        let mut print_cb;\n        let print_writer = match &print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::new(cb);\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n\n        let runner = self.runner.clone();\n        let print_writer = SendWrapper::new(print_writer);\n\n        // Helper macro to start execution with GIL released\n        macro_rules! start_impl {\n            ($tracker:expr) => {{\n                py.detach(|| runner.start(input_values, $tracker, print_writer.take()))\n                    .map_err(|e| MontyError::new_err(py, e))?\n            }};\n        }\n\n        // Branch on limits (different generic types)\n        let progress = if let Some(limits) = limits {\n            let tracker = PySignalTracker::new(LimitedTracker::new(extract_limits(limits)?));\n            EitherProgress::Limited(start_impl!(tracker))\n        } else {\n            let tracker = PySignalTracker::new(NoLimitTracker);\n            EitherProgress::NoLimit(start_impl!(tracker))\n        };\n        progress.progress_or_complete(\n            py,\n            self.script_name.clone(),\n            print_callback.map(Bound::unbind),\n            dc_registry,\n        )\n    }\n\n    /// Serializes the Monty instance to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `Monty.load()`.\n    /// This allows caching parsed code to avoid re-parsing on subsequent runs.\n    ///\n    /// # Returns\n    /// Bytes containing the serialized Monty instance.\n    ///\n    /// # Raises\n    /// `ValueError` if serialization fails.\n    fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {\n        let serialized = SerializedMonty {\n            runner: self.runner.clone(),\n            script_name: self.script_name.clone(),\n            input_names: self.input_names.clone(),\n        };\n        let bytes = postcard::to_allocvec(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))?;\n        Ok(PyBytes::new(py, &bytes))\n    }\n\n    /// Deserializes a Monty instance from binary format.\n    ///\n    /// # Arguments\n    /// * `data` - The serialized Monty data from `dump()`\n    /// * `dataclass_registry` - Optional list of dataclasses to register\n    ///\n    /// # Returns\n    /// A new Monty instance.\n    ///\n    /// # Raises\n    /// `ValueError` if deserialization fails.\n    #[staticmethod]\n    #[pyo3(signature = (data, *, dataclass_registry=None))]\n    fn load(\n        py: Python<'_>,\n        data: &Bound<'_, PyBytes>,\n        dataclass_registry: Option<&Bound<'_, PyList>>,\n    ) -> PyResult<Self> {\n        let bytes = data.as_bytes();\n        let serialized: SerializedMonty =\n            postcard::from_bytes(bytes).map_err(|e| PyValueError::new_err(e.to_string()))?;\n\n        Ok(Self {\n            runner: serialized.runner,\n            script_name: serialized.script_name,\n            input_names: serialized.input_names,\n            dc_registry: DcRegistry::from_list(py, dataclass_registry)?,\n        })\n    }\n\n    fn __repr__(&self) -> String {\n        let lines = self.runner.code().lines().count();\n        let mut s = format!(\n            \"Monty(<{} line{} of code>, script_name='{}'\",\n            lines,\n            if lines == 1 { \"\" } else { \"s\" },\n            self.script_name\n        );\n        if !self.input_names.is_empty() {\n            write!(s, \", inputs={:?}\", self.input_names).unwrap();\n        }\n        s.push(')');\n        s\n    }\n}\n\nfn py_type_check(py: Python<'_>, code: &str, script_name: &str, type_stubs: Option<&str>) -> PyResult<()> {\n    let type_stubs = type_stubs.map(|type_stubs| SourceFile::new(type_stubs, \"type_stubs.pyi\"));\n\n    let opt_diagnostics =\n        type_check(&SourceFile::new(code, script_name), type_stubs.as_ref()).map_err(PyRuntimeError::new_err)?;\n\n    if let Some(diagnostic) = opt_diagnostics {\n        Err(MontyTypingError::new_err(py, diagnostic))\n    } else {\n        Ok(())\n    }\n}\n\nimpl PyMonty {\n    /// Extracts input values from a Python dict in the order they were declared.\n    ///\n    /// Validates that all required inputs are provided. Any dataclass inputs are\n    /// automatically registered in `dc_registry` via `py_to_monty` so they can be\n    /// properly reconstructed on output.\n    fn extract_input_values(\n        &self,\n        inputs: Option<&Bound<'_, PyDict>>,\n        dc_registry: &DcRegistry,\n    ) -> PyResult<Vec<::monty::MontyObject>> {\n        if self.input_names.is_empty() {\n            if inputs.is_some() {\n                return Err(PyTypeError::new_err(\n                    \"No input variables declared but inputs dict was provided\",\n                ));\n            }\n            return Ok(vec![]);\n        }\n\n        let Some(inputs) = inputs else {\n            return Err(PyTypeError::new_err(format!(\n                \"Missing required inputs: {:?}\",\n                self.input_names\n            )));\n        };\n\n        // Extract values in declaration order\n        self.input_names\n            .iter()\n            .map(|name| {\n                let value = inputs\n                    .get_item(name)?\n                    .ok_or_else(|| PyKeyError::new_err(format!(\"Missing required input: '{name}'\")))?;\n                py_to_monty(&value, dc_registry)\n            })\n            .collect::<PyResult<_>>()\n    }\n\n    /// Runs code with a generic resource tracker, releasing the GIL during execution.\n    ///\n    /// Takes explicit field references instead of `&mut self` so that `run()` can\n    /// remain `&self` (required for concurrent thread access in PyO3).\n    fn run_impl(\n        &self,\n        py: Python<'_>,\n        input_values: Vec<MontyObject>,\n        tracker: impl ResourceTracker + Send,\n        external_functions: Option<&Bound<'_, PyDict>>,\n        os: Option<&Bound<'_, PyAny>>,\n        print_output: PrintWriter<'_>,\n    ) -> PyResult<Py<PyAny>> {\n        // wrap print_output in SendWrapper so that it can be accessed inside the py.detach calls despite\n        // no `Send` bound - py.detach() is overly restrictive to prevent `Bound` types going inside\n        let mut print_output = SendWrapper::new(print_output);\n\n        // Check if any inputs contain dataclasses (including nested in containers) —\n        // if so, we need the iterative path because method calls could happen lazily\n        // and need to be dispatched to the host.\n        let has_dataclass_inputs = || input_values.iter().any(contains_dataclass);\n\n        if external_functions.is_none() && os.is_none() && !has_dataclass_inputs() {\n            return match py.detach(|| self.runner.run(input_values, tracker, print_output.reborrow())) {\n                Ok(v) => monty_to_py(py, &v, &self.dc_registry),\n                Err(err) => Err(MontyError::new_err(py, err)),\n            };\n        }\n        // Clone the runner since start() consumes it - allows reuse of the parsed code\n        let runner = self.runner.clone();\n        let mut progress = py\n            .detach(|| runner.start(input_values, tracker, print_output.reborrow()))\n            .map_err(|e| MontyError::new_err(py, e))?;\n\n        loop {\n            match progress {\n                RunProgress::Complete(result) => return monty_to_py(py, &result, &self.dc_registry),\n                RunProgress::FunctionCall(call) => {\n                    // Dataclass method calls have method_call=true and the first arg is the instance\n                    let return_value = if call.method_call {\n                        dispatch_method_call(py, &call.function_name, &call.args, &call.kwargs, &self.dc_registry)\n                    } else if let Some(ext_fns) = external_functions {\n                        let registry = ExternalFunctionRegistry::new(py, ext_fns, &self.dc_registry);\n                        registry.call(&call.function_name, &call.args, &call.kwargs)\n                    } else {\n                        return Err(PyRuntimeError::new_err(format!(\n                            \"External function '{}' called but no external_functions provided\",\n                            call.function_name\n                        )));\n                    };\n\n                    progress = py\n                        .detach(|| call.resume(return_value, print_output.reborrow()))\n                        .map_err(|e| MontyError::new_err(py, e))?;\n                }\n                RunProgress::NameLookup(lookup) => {\n                    let result = if let Some(ext_fns) = external_functions\n                        && let Some(value) = ext_fns.get_item(&lookup.name)?\n                    {\n                        NameLookupResult::Value(MontyObject::Function {\n                            name: lookup.name.clone(),\n                            docstring: get_docstring(&value),\n                        })\n                    } else {\n                        NameLookupResult::Undefined\n                    };\n\n                    progress = py\n                        .detach(|| lookup.resume(result, print_output.reborrow()))\n                        .map_err(|e| MontyError::new_err(py, e))?;\n                }\n                RunProgress::ResolveFutures(_) => {\n                    return Err(PyRuntimeError::new_err(\"async futures not supported with `Monty.run`\"));\n                }\n                RunProgress::OsCall(call) => {\n                    let result: ExtFunctionResult = if let Some(os_callback) = os {\n                        // Convert args to Python\n                        let py_args: Vec<Py<PyAny>> = call\n                            .args\n                            .iter()\n                            .map(|arg| monty_to_py(py, arg, &self.dc_registry))\n                            .collect::<PyResult<_>>()?;\n                        let py_args_tuple = PyTuple::new(py, py_args)?;\n\n                        // Convert kwargs to Python dict\n                        let py_kwargs = PyDict::new(py);\n                        for (k, v) in &call.kwargs {\n                            py_kwargs.set_item(\n                                monty_to_py(py, k, &self.dc_registry)?,\n                                monty_to_py(py, v, &self.dc_registry)?,\n                            )?;\n                        }\n\n                        // call the os callback, if an exception is raised, return it to monty\n                        match os_callback.call1((call.function.to_string(), py_args_tuple, py_kwargs)) {\n                            Ok(result) => py_to_monty(&result, &self.dc_registry)?.into(),\n                            Err(err) => exc_py_to_monty(py, &err).into(),\n                        }\n                    } else {\n                        MontyException::new(\n                            ExcType::NotImplementedError,\n                            Some(format!(\"OS function '{}' not implemented\", call.function)),\n                        )\n                        .into()\n                    };\n\n                    progress = py\n                        .detach(|| call.resume(result, print_output.reborrow()))\n                        .map_err(|e| MontyError::new_err(py, e))?;\n                }\n            }\n        }\n    }\n}\n\n/// pyclass doesn't support generic types, hence hard coding the generics\n#[derive(Debug)]\npub(crate) enum EitherProgress {\n    NoLimit(RunProgress<PySignalTracker<NoLimitTracker>>),\n    Limited(RunProgress<PySignalTracker<LimitedTracker>>),\n    /// REPL progress with back-reference to the owning `PyMontyRepl` for auto-restore.\n    ReplNoLimit(ReplProgress<PySignalTracker<NoLimitTracker>>, Py<PyMontyRepl>),\n    /// REPL progress with back-reference to the owning `PyMontyRepl` for auto-restore.\n    ReplLimited(ReplProgress<PySignalTracker<LimitedTracker>>, Py<PyMontyRepl>),\n}\n\nimpl EitherProgress {\n    /// Converts progress into the appropriate Python object:\n    /// function snapshot, name lookup snapshot, future snapshot, or complete.\n    pub(crate) fn progress_or_complete(\n        self,\n        py: Python<'_>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n    ) -> PyResult<Bound<'_, PyAny>> {\n        match self {\n            Self::NoLimit(p) => run_progress_to_py(py, p, script_name, print_callback, dc_registry),\n            Self::Limited(p) => run_progress_to_py(py, p, script_name, print_callback, dc_registry),\n            Self::ReplNoLimit(p, owner) => repl_progress_to_py(py, p, script_name, print_callback, dc_registry, owner),\n            Self::ReplLimited(p, owner) => repl_progress_to_py(py, p, script_name, print_callback, dc_registry, owner),\n        }\n    }\n}\n\n/// Converts a `RunProgress<T>` into the appropriate Python snapshot type.\nfn run_progress_to_py<T: ResourceTracker>(\n    py: Python<'_>,\n    progress: RunProgress<T>,\n    script_name: String,\n    print_callback: Option<Py<PyAny>>,\n    dc_registry: DcRegistry,\n) -> PyResult<Bound<'_, PyAny>>\nwhere\n    EitherFunctionSnapshot: FromFunctionCall<T> + FromOsCall<T>,\n    EitherLookupSnapshot: FromNameLookup<T>,\n    EitherFutureSnapshot: FromResolveFutures<T>,\n{\n    match progress {\n        RunProgress::Complete(result) => PyMontyComplete::create(py, &result, &dc_registry),\n        RunProgress::FunctionCall(call) => {\n            PyFunctionSnapshot::function_call(py, call, script_name, print_callback, dc_registry)\n        }\n        RunProgress::OsCall(call) => PyFunctionSnapshot::os_call(py, call, script_name, print_callback, dc_registry),\n        RunProgress::ResolveFutures(state) => {\n            PyFutureSnapshot::new_py_any(py, state, script_name, print_callback, dc_registry)\n        }\n        RunProgress::NameLookup(lookup) => {\n            PyNameLookupSnapshot::new_py_any(py, lookup, script_name, print_callback, dc_registry)\n        }\n    }\n}\n\n/// Converts a `ReplProgress<T>` into the appropriate Python snapshot type.\n///\n/// On completion, restores the REPL state into `repl_owner` before returning `MontyComplete`.\n/// The `repl_owner` is propagated into snapshot enum variants so the chain can continue.\nfn repl_progress_to_py<T: ResourceTracker>(\n    py: Python<'_>,\n    progress: ReplProgress<T>,\n    script_name: String,\n    print_callback: Option<Py<PyAny>>,\n    dc_registry: DcRegistry,\n    repl_owner: Py<PyMontyRepl>,\n) -> PyResult<Bound<'_, PyAny>>\nwhere\n    EitherFunctionSnapshot: FromReplFunctionCall<T> + FromReplOsCall<T>,\n    EitherLookupSnapshot: FromReplNameLookup<T>,\n    EitherFutureSnapshot: FromReplResolveFutures<T>,\n    EitherRepl: FromCoreRepl<T>,\n{\n    match progress {\n        ReplProgress::Complete { repl, value } => {\n            repl_owner.get().put_repl(EitherRepl::from_core(repl));\n            PyMontyComplete::create(py, &value, &dc_registry)\n        }\n        ReplProgress::FunctionCall(call) => {\n            PyFunctionSnapshot::repl_function_call(py, call, script_name, print_callback, dc_registry, repl_owner)\n        }\n        ReplProgress::OsCall(call) => {\n            PyFunctionSnapshot::repl_os_call(py, call, script_name, print_callback, dc_registry, repl_owner)\n        }\n        ReplProgress::NameLookup(lookup) => {\n            let variable_name = lookup.name.clone();\n            PyNameLookupSnapshot::repl_name_lookup(\n                py,\n                lookup,\n                script_name,\n                print_callback,\n                dc_registry,\n                repl_owner,\n                variable_name,\n            )\n        }\n        ReplProgress::ResolveFutures(state) => {\n            PyFutureSnapshot::repl_resolve_futures(py, state, script_name, print_callback, dc_registry, repl_owner)\n        }\n    }\n}\n\n/// Runtime execution snapshot, holds either a `FunctionCall` or `OsCall` for both\n/// resource tracker variants since pyclass structs can't be generic.\n///\n/// Also holds REPL variants (`ReplFunctionCall`, `ReplOsCall`) for `MontyRepl.feed_start()`.\n/// REPL variants carry a `Py<PyMontyRepl>` back-reference so the REPL can be auto-restored\n/// on completion or error.\n///\n/// Used internally by `PyFunctionSnapshot` to store execution state. Both `FunctionCall`\n/// and `OsCall` have the same `resume()` signature, so we dispatch to the appropriate\n/// inner type based on the variant.\n///\n/// The `Done` variant indicates the snapshot has been consumed.\n///\n/// Serde: REPL variants serialize as their non-REPL counterparts (stripping the owner).\n/// Deserialization always produces non-REPL variants.\n#[derive(Debug)]\npub(crate) enum EitherFunctionSnapshot {\n    // Run variants (from Monty.start())\n    NoLimitFn(FunctionCall<PySignalTracker<NoLimitTracker>>),\n    NoLimitOs(OsCall<PySignalTracker<NoLimitTracker>>),\n    LimitedFn(FunctionCall<PySignalTracker<LimitedTracker>>),\n    LimitedOs(OsCall<PySignalTracker<LimitedTracker>>),\n    // REPL variants (from MontyRepl.feed_start()) — carry the REPL owner\n    ReplNoLimitFn(ReplFunctionCall<PySignalTracker<NoLimitTracker>>, Py<PyMontyRepl>),\n    ReplNoLimitOs(ReplOsCall<PySignalTracker<NoLimitTracker>>, Py<PyMontyRepl>),\n    ReplLimitedFn(ReplFunctionCall<PySignalTracker<LimitedTracker>>, Py<PyMontyRepl>),\n    ReplLimitedOs(ReplOsCall<PySignalTracker<LimitedTracker>>, Py<PyMontyRepl>),\n    /// Sentinel indicating the snapshot has been consumed via `resume()`.\n    Done,\n}\n\n/// Helper trait for wrapping `FunctionCall<T>` into `EitherFunctionSnapshot`.\ntrait FromFunctionCall<T: ResourceTracker> {\n    /// Wraps a function call into the appropriate variant.\n    fn from_fn(call: FunctionCall<T>) -> Self;\n}\n\nimpl FromFunctionCall<PySignalTracker<NoLimitTracker>> for EitherFunctionSnapshot {\n    fn from_fn(call: FunctionCall<PySignalTracker<NoLimitTracker>>) -> Self {\n        Self::NoLimitFn(call)\n    }\n}\n\nimpl FromFunctionCall<PySignalTracker<LimitedTracker>> for EitherFunctionSnapshot {\n    fn from_fn(call: FunctionCall<PySignalTracker<LimitedTracker>>) -> Self {\n        Self::LimitedFn(call)\n    }\n}\n\n/// Helper trait for wrapping `OsCall<T>` into `EitherFunctionSnapshot`.\ntrait FromOsCall<T: ResourceTracker> {\n    /// Wraps an OS call into the appropriate variant.\n    fn from_os(call: OsCall<T>) -> Self;\n}\n\nimpl FromOsCall<PySignalTracker<NoLimitTracker>> for EitherFunctionSnapshot {\n    fn from_os(call: OsCall<PySignalTracker<NoLimitTracker>>) -> Self {\n        Self::NoLimitOs(call)\n    }\n}\n\nimpl FromOsCall<PySignalTracker<LimitedTracker>> for EitherFunctionSnapshot {\n    fn from_os(call: OsCall<PySignalTracker<LimitedTracker>>) -> Self {\n        Self::LimitedOs(call)\n    }\n}\n\n/// Helper trait for wrapping `ReplFunctionCall<T>` into `EitherFunctionSnapshot`.\ntrait FromReplFunctionCall<T: ResourceTracker> {\n    /// Wraps a REPL function call into the appropriate variant.\n    fn from_repl_fn(call: ReplFunctionCall<T>, owner: Py<PyMontyRepl>) -> Self;\n}\n\nimpl FromReplFunctionCall<PySignalTracker<NoLimitTracker>> for EitherFunctionSnapshot {\n    fn from_repl_fn(call: ReplFunctionCall<PySignalTracker<NoLimitTracker>>, owner: Py<PyMontyRepl>) -> Self {\n        Self::ReplNoLimitFn(call, owner)\n    }\n}\n\nimpl FromReplFunctionCall<PySignalTracker<LimitedTracker>> for EitherFunctionSnapshot {\n    fn from_repl_fn(call: ReplFunctionCall<PySignalTracker<LimitedTracker>>, owner: Py<PyMontyRepl>) -> Self {\n        Self::ReplLimitedFn(call, owner)\n    }\n}\n\n/// Helper trait for wrapping `ReplOsCall<T>` into `EitherFunctionSnapshot`.\ntrait FromReplOsCall<T: ResourceTracker> {\n    /// Wraps a REPL OS call into the appropriate variant.\n    fn from_repl_os(call: ReplOsCall<T>, owner: Py<PyMontyRepl>) -> Self;\n}\n\nimpl FromReplOsCall<PySignalTracker<NoLimitTracker>> for EitherFunctionSnapshot {\n    fn from_repl_os(call: ReplOsCall<PySignalTracker<NoLimitTracker>>, owner: Py<PyMontyRepl>) -> Self {\n        Self::ReplNoLimitOs(call, owner)\n    }\n}\n\nimpl FromReplOsCall<PySignalTracker<LimitedTracker>> for EitherFunctionSnapshot {\n    fn from_repl_os(call: ReplOsCall<PySignalTracker<LimitedTracker>>, owner: Py<PyMontyRepl>) -> Self {\n        Self::ReplLimitedOs(call, owner)\n    }\n}\n\n/// Snapshot generated during execution when monty yields to the host for a function call.\n#[pyclass(name = \"FunctionSnapshot\", module = \"pydantic_monty\")]\n#[derive(Debug)]\npub struct PyFunctionSnapshot {\n    snapshot: Mutex<EitherFunctionSnapshot>,\n    print_callback: Option<Py<PyAny>>,\n    dc_registry: DcRegistry,\n\n    /// Name of the script being executed\n    #[pyo3(get)]\n    pub script_name: String,\n\n    /// Whether this call refers to an OS function\n    #[pyo3(get)]\n    pub is_os_function: bool,\n\n    /// Whether this call is a dataclass method call (first arg is `self`)\n    #[pyo3(get)]\n    pub is_method_call: bool,\n\n    /// The name of the function being called.\n    #[pyo3(get)]\n    pub function_name: String,\n    /// The positional arguments passed to the function.\n    #[pyo3(get)]\n    pub args: Py<PyTuple>,\n    /// The keyword arguments passed to the function (key, value pairs).\n    #[pyo3(get)]\n    pub kwargs: Py<PyDict>,\n    /// The unique identifier for this call\n    #[pyo3(get)]\n    pub call_id: u32,\n}\n\nimpl PyFunctionSnapshot {\n    /// Creates a `PyFunctionSnapshot` for an external function call.\n    ///\n    /// Extracts display fields from the `FunctionCall` before moving it into\n    /// `EitherSnapshot` via the provided `wrap` closure.\n    fn function_call<T: ResourceTracker>(\n        py: Python<'_>,\n        call: FunctionCall<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherFunctionSnapshot: FromFunctionCall<T>,\n    {\n        let function_name = call.function_name.clone();\n        let call_id = call.call_id;\n        let method_call = call.method_call;\n        let items: PyResult<Vec<Py<PyAny>>> = call\n            .args\n            .iter()\n            .map(|item| monty_to_py(py, item, &dc_registry))\n            .collect();\n        let dict = PyDict::new(py);\n        for (k, v) in &call.kwargs {\n            dict.set_item(monty_to_py(py, k, &dc_registry)?, monty_to_py(py, v, &dc_registry)?)?;\n        }\n\n        let slf = Self {\n            snapshot: Mutex::new(EitherFunctionSnapshot::from_fn(call)),\n            print_callback,\n            script_name,\n            is_os_function: false,\n            is_method_call: method_call,\n            function_name,\n            args: PyTuple::new(py, items?)?.unbind(),\n            kwargs: dict.unbind(),\n            call_id,\n            dc_registry,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Creates a `PyFunctionSnapshot` for an OS-level call.\n    ///\n    /// Extracts display fields from the `OsCall` before moving it into\n    /// `EitherSnapshot` via the provided `wrap` closure.\n    fn os_call<T: ResourceTracker>(\n        py: Python<'_>,\n        call: OsCall<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherFunctionSnapshot: FromOsCall<T>,\n    {\n        let function_name = call.function.to_string();\n        let call_id = call.call_id;\n        let items: PyResult<Vec<Py<PyAny>>> = call\n            .args\n            .iter()\n            .map(|item| monty_to_py(py, item, &dc_registry))\n            .collect();\n        let dict = PyDict::new(py);\n        for (k, v) in &call.kwargs {\n            dict.set_item(monty_to_py(py, k, &dc_registry)?, monty_to_py(py, v, &dc_registry)?)?;\n        }\n\n        let slf = Self {\n            snapshot: Mutex::new(EitherFunctionSnapshot::from_os(call)),\n            print_callback,\n            script_name,\n            is_os_function: true,\n            is_method_call: false,\n            function_name,\n            args: PyTuple::new(py, items?)?.unbind(),\n            kwargs: dict.unbind(),\n            call_id,\n            dc_registry,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Creates a `PyFunctionSnapshot` for a REPL external function call.\n    fn repl_function_call<T: ResourceTracker>(\n        py: Python<'_>,\n        call: ReplFunctionCall<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        repl_owner: Py<PyMontyRepl>,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherFunctionSnapshot: FromReplFunctionCall<T>,\n    {\n        let function_name = call.function_name.clone();\n        let call_id = call.call_id;\n        let method_call = call.method_call;\n        let items: PyResult<Vec<Py<PyAny>>> = call\n            .args\n            .iter()\n            .map(|item| monty_to_py(py, item, &dc_registry))\n            .collect();\n        let dict = PyDict::new(py);\n        for (k, v) in &call.kwargs {\n            dict.set_item(monty_to_py(py, k, &dc_registry)?, monty_to_py(py, v, &dc_registry)?)?;\n        }\n\n        let slf = Self {\n            snapshot: Mutex::new(EitherFunctionSnapshot::from_repl_fn(call, repl_owner)),\n            print_callback,\n            script_name,\n            is_os_function: false,\n            is_method_call: method_call,\n            function_name,\n            args: PyTuple::new(py, items?)?.unbind(),\n            kwargs: dict.unbind(),\n            call_id,\n            dc_registry,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Creates a `PyFunctionSnapshot` for a REPL OS-level call.\n    fn repl_os_call<T: ResourceTracker>(\n        py: Python<'_>,\n        call: ReplOsCall<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        repl_owner: Py<PyMontyRepl>,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherFunctionSnapshot: FromReplOsCall<T>,\n    {\n        let function_name = call.function.to_string();\n        let call_id = call.call_id;\n        let items: PyResult<Vec<Py<PyAny>>> = call\n            .args\n            .iter()\n            .map(|item| monty_to_py(py, item, &dc_registry))\n            .collect();\n        let dict = PyDict::new(py);\n        for (k, v) in &call.kwargs {\n            dict.set_item(monty_to_py(py, k, &dc_registry)?, monty_to_py(py, v, &dc_registry)?)?;\n        }\n\n        let slf = Self {\n            snapshot: Mutex::new(EitherFunctionSnapshot::from_repl_os(call, repl_owner)),\n            print_callback,\n            script_name,\n            is_os_function: true,\n            is_method_call: false,\n            function_name,\n            args: PyTuple::new(py, items?)?.unbind(),\n            kwargs: dict.unbind(),\n            call_id,\n            dc_registry,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Constructs a `PyFunctionSnapshot` from deserialized parts.\n    ///\n    /// Used by `load_snapshot` and `load_repl_snapshot` to reconstruct snapshot objects.\n    #[expect(clippy::too_many_arguments)]\n    pub(crate) fn from_deserialized(\n        py: Python<'_>,\n        snapshot: EitherFunctionSnapshot,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        script_name: String,\n        is_os_function: bool,\n        is_method_call: bool,\n        function_name: String,\n        args: Py<PyTuple>,\n        kwargs: Py<PyDict>,\n        call_id: u32,\n    ) -> PyResult<Bound<'_, PyAny>> {\n        let slf = Self {\n            snapshot: Mutex::new(snapshot),\n            print_callback,\n            dc_registry,\n            script_name,\n            is_os_function,\n            is_method_call,\n            function_name,\n            args,\n            kwargs,\n            call_id,\n        };\n        slf.into_bound_py_any(py)\n    }\n}\n\n#[pymethods]\nimpl PyFunctionSnapshot {\n    /// Resumes execution with either a return value, exception or future.\n    ///\n    /// Exactly one of `return_value`, `exception` or `future` must be provided as a keyword argument.\n    ///\n    /// # Raises\n    /// * `TypeError` if both arguments are provided, or neither\n    /// * `RuntimeError` if the snapshot has already been resumed\n    #[pyo3(signature = (**kwargs))]\n    pub fn resume<'py>(&self, py: Python<'py>, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Bound<'py, PyAny>> {\n        const ARGS_ERROR: &str = \"resume() accepts either return_value or exception, not both\";\n\n        let mut snapshot = self\n            .snapshot\n            .lock()\n            .map_err(|_| PyRuntimeError::new_err(\"Snapshot is currently being resumed by another thread\"))?;\n\n        let snapshot = std::mem::replace(&mut *snapshot, EitherFunctionSnapshot::Done);\n        let Some(kwargs) = kwargs else {\n            return Err(PyTypeError::new_err(ARGS_ERROR));\n        };\n        let external_result = extract_external_result(py, kwargs, ARGS_ERROR, &self.dc_registry, self.call_id)?;\n\n        // Build print writer before detaching - clone_ref needs py token\n        let mut print_cb;\n        let print_writer = match &self.print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::from_py(cb.clone_ref(py));\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n        // wrap print_writer in SendWrapper so that it can be accessed inside the py.detach calls despite\n        // no `Send` bound - py.detach() is overly restrictive to prevent `Bound` types going inside\n        let mut print_writer = SendWrapper::new(print_writer);\n\n        let progress = match snapshot {\n            EitherFunctionSnapshot::NoLimitFn(call) => {\n                let result = py.detach(|| call.resume(external_result, print_writer.reborrow()));\n                EitherProgress::NoLimit(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherFunctionSnapshot::NoLimitOs(call) => {\n                let result = py.detach(|| call.resume(external_result, print_writer.reborrow()));\n                EitherProgress::NoLimit(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherFunctionSnapshot::LimitedFn(call) => {\n                let result = py.detach(|| call.resume(external_result, print_writer.reborrow()));\n                EitherProgress::Limited(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherFunctionSnapshot::LimitedOs(call) => {\n                let result = py.detach(|| call.resume(external_result, print_writer.reborrow()));\n                EitherProgress::Limited(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherFunctionSnapshot::ReplNoLimitFn(call, owner) => {\n                let result = py\n                    .detach(|| call.resume(external_result, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplNoLimit(result, owner)\n            }\n            EitherFunctionSnapshot::ReplNoLimitOs(call, owner) => {\n                let result = py\n                    .detach(|| call.resume(external_result, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplNoLimit(result, owner)\n            }\n            EitherFunctionSnapshot::ReplLimitedFn(call, owner) => {\n                let result = py\n                    .detach(|| call.resume(external_result, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplLimited(result, owner)\n            }\n            EitherFunctionSnapshot::ReplLimitedOs(call, owner) => {\n                let result = py\n                    .detach(|| call.resume(external_result, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplLimited(result, owner)\n            }\n            EitherFunctionSnapshot::Done => return Err(PyRuntimeError::new_err(\"Progress already resumed\")),\n        };\n\n        let dc_registry = self.dc_registry.clone_ref(py);\n        progress.progress_or_complete(\n            py,\n            self.script_name.clone(),\n            self.print_callback.as_ref().map(|cb| cb.clone_ref(py)),\n            dc_registry,\n        )\n    }\n\n    /// Serializes the FunctionSnapshot instance to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `load_snapshot()`\n    /// or `load_repl_snapshot()`. REPL snapshots automatically include the REPL state.\n    ///\n    /// Note: The `print_callback` is not serialized and must be re-provided when loading.\n    ///\n    /// # Returns\n    /// Bytes containing the serialized FunctionSnapshot instance.\n    ///\n    /// # Raises\n    /// `ValueError` if serialization fails.\n    /// `RuntimeError` if the progress has already been resumed.\n    fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {\n        let bytes = crate::serialization::dump_function_snapshot(\n            py,\n            &self.snapshot,\n            &self.script_name,\n            self.is_os_function,\n            self.is_method_call,\n            &self.function_name,\n            &self.args,\n            &self.kwargs,\n            self.call_id,\n            &self.dc_registry,\n        )?;\n        Ok(PyBytes::new(py, &bytes))\n    }\n\n    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {\n        Ok(format!(\n            \"FunctionSnapshot(script_name='{}', function_name='{}', args={}, kwargs={})\",\n            self.script_name,\n            self.function_name,\n            self.args.bind(py).repr()?,\n            self.kwargs.bind(py).repr()?\n        ))\n    }\n}\n\n/// Runtime execution snapshot, holds a `NameLookup` for both\n/// resource tracker variants since pyclass structs can't be generic.\n///\n/// Also holds REPL variants with `Py<PyMontyRepl>` for `MontyRepl.feed_start()`.\n///\n/// The `Done` variant indicates the snapshot has been consumed.\n#[derive(Debug)]\npub(crate) enum EitherLookupSnapshot {\n    NoLimit(NameLookup<PySignalTracker<NoLimitTracker>>),\n    Limited(NameLookup<PySignalTracker<LimitedTracker>>),\n    ReplNoLimit(ReplNameLookup<PySignalTracker<NoLimitTracker>>, Py<PyMontyRepl>),\n    ReplLimited(ReplNameLookup<PySignalTracker<LimitedTracker>>, Py<PyMontyRepl>),\n    /// Sentinel indicating the snapshot has been consumed via `resume()`.\n    Done,\n}\n\n/// Helper trait for wrapping `NameLookup<T>` into `EitherLookupSnapshot`.\ntrait FromNameLookup<T: ResourceTracker> {\n    /// Wraps a name lookup into the appropriate variant.\n    fn from_name_lookup(lookup: NameLookup<T>) -> Self;\n}\n\nimpl FromNameLookup<PySignalTracker<NoLimitTracker>> for EitherLookupSnapshot {\n    fn from_name_lookup(lookup: NameLookup<PySignalTracker<NoLimitTracker>>) -> Self {\n        Self::NoLimit(lookup)\n    }\n}\n\nimpl FromNameLookup<PySignalTracker<LimitedTracker>> for EitherLookupSnapshot {\n    fn from_name_lookup(lookup: NameLookup<PySignalTracker<LimitedTracker>>) -> Self {\n        Self::Limited(lookup)\n    }\n}\n\n/// Helper trait for wrapping `ReplNameLookup<T>` into `EitherLookupSnapshot`.\ntrait FromReplNameLookup<T: ResourceTracker> {\n    /// Wraps a REPL name lookup into the appropriate variant.\n    fn from_repl_name_lookup(lookup: ReplNameLookup<T>, owner: Py<PyMontyRepl>) -> Self;\n}\n\nimpl FromReplNameLookup<PySignalTracker<NoLimitTracker>> for EitherLookupSnapshot {\n    fn from_repl_name_lookup(lookup: ReplNameLookup<PySignalTracker<NoLimitTracker>>, owner: Py<PyMontyRepl>) -> Self {\n        Self::ReplNoLimit(lookup, owner)\n    }\n}\n\nimpl FromReplNameLookup<PySignalTracker<LimitedTracker>> for EitherLookupSnapshot {\n    fn from_repl_name_lookup(lookup: ReplNameLookup<PySignalTracker<LimitedTracker>>, owner: Py<PyMontyRepl>) -> Self {\n        Self::ReplLimited(lookup, owner)\n    }\n}\n\n/// Snapshot generated during execution when monty yields to the host for a name lookup.\n#[pyclass(name = \"NameLookupSnapshot\", module = \"pydantic_monty\")]\n#[derive(Debug)]\npub struct PyNameLookupSnapshot {\n    snapshot: Mutex<EitherLookupSnapshot>,\n    print_callback: Option<Py<PyAny>>,\n    dc_registry: DcRegistry,\n\n    /// Name of the script being executed\n    #[pyo3(get)]\n    pub script_name: String,\n\n    /// Name of the variable being looked up\n    #[pyo3(get)]\n    pub variable_name: String,\n}\n\nimpl PyNameLookupSnapshot {\n    /// Creates a `PyNameLookupSnapshot` for an external function call.\n    ///\n    /// Extracts display fields from the `FunctionCall` before moving it into\n    /// `EitherSnapshot` via the provided `wrap` closure.\n    fn new_py_any<T: ResourceTracker>(\n        py: Python<'_>,\n        lookup: NameLookup<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherLookupSnapshot: FromNameLookup<T>,\n    {\n        let variable_name = lookup.name.clone();\n\n        let slf = Self {\n            snapshot: Mutex::new(EitherLookupSnapshot::from_name_lookup(lookup)),\n            print_callback,\n            dc_registry,\n            script_name,\n            variable_name,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Creates a `PyNameLookupSnapshot` for a REPL name lookup.\n    fn repl_name_lookup<T: ResourceTracker>(\n        py: Python<'_>,\n        lookup: ReplNameLookup<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        repl_owner: Py<PyMontyRepl>,\n        variable_name: String,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherLookupSnapshot: FromReplNameLookup<T>,\n    {\n        let slf = Self {\n            snapshot: Mutex::new(EitherLookupSnapshot::from_repl_name_lookup(lookup, repl_owner)),\n            print_callback,\n            dc_registry,\n            script_name,\n            variable_name,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Constructs a `PyNameLookupSnapshot` from deserialized parts.\n    pub(crate) fn from_deserialized(\n        py: Python<'_>,\n        snapshot: EitherLookupSnapshot,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        script_name: String,\n        variable_name: String,\n    ) -> PyResult<Bound<'_, PyAny>> {\n        let slf = Self {\n            snapshot: Mutex::new(snapshot),\n            print_callback,\n            dc_registry,\n            script_name,\n            variable_name,\n        };\n        slf.into_bound_py_any(py)\n    }\n}\n\n#[pymethods]\nimpl PyNameLookupSnapshot {\n    /// Resumes execution with either a value or undefined.\n    #[pyo3(signature = (**kwargs))]\n    pub fn resume<'py>(&self, py: Python<'py>, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Bound<'py, PyAny>> {\n        let mut snapshot = self\n            .snapshot\n            .lock()\n            .map_err(|_| PyRuntimeError::new_err(\"Snapshot is currently being resumed by another thread\"))?;\n\n        let snapshot = std::mem::replace(&mut *snapshot, EitherLookupSnapshot::Done);\n        let lookup_result = if let Some(kwargs) = kwargs\n            && let Some(value) = kwargs.get_item(intern!(py, \"value\"))?\n        {\n            NameLookupResult::Value(py_to_monty(&value, &self.dc_registry)?)\n        } else {\n            NameLookupResult::Undefined\n        };\n\n        // Build print writer before detaching - clone_ref needs py token\n        let mut print_cb;\n        let print_writer = match &self.print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::from_py(cb.clone_ref(py));\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n        let mut print_writer = SendWrapper::new(print_writer);\n\n        let progress = match snapshot {\n            EitherLookupSnapshot::NoLimit(snapshot) => {\n                let result = py.detach(|| snapshot.resume(lookup_result, print_writer.reborrow()));\n                EitherProgress::NoLimit(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherLookupSnapshot::Limited(snapshot) => {\n                let result = py.detach(|| snapshot.resume(lookup_result, print_writer.reborrow()));\n                EitherProgress::Limited(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherLookupSnapshot::ReplNoLimit(snapshot, owner) => {\n                let result = py\n                    .detach(|| snapshot.resume(lookup_result, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplNoLimit(result, owner)\n            }\n            EitherLookupSnapshot::ReplLimited(snapshot, owner) => {\n                let result = py\n                    .detach(|| snapshot.resume(lookup_result, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplLimited(result, owner)\n            }\n            EitherLookupSnapshot::Done => return Err(PyRuntimeError::new_err(\"Progress already resumed\")),\n        };\n\n        // Clone the Arc handle for the next snapshot/complete\n        let dc_registry = self.dc_registry.clone_ref(py);\n        progress.progress_or_complete(\n            py,\n            self.script_name.clone(),\n            self.print_callback.as_ref().map(|cb| cb.clone_ref(py)),\n            dc_registry,\n        )\n    }\n\n    /// Serializes the NameLookupSnapshot instance to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `load_snapshot()`\n    /// or `load_repl_snapshot()`. REPL snapshots automatically include the REPL state.\n    ///\n    /// Note: The `print_callback` is not serialized and must be re-provided when loading.\n    ///\n    /// # Returns\n    /// Bytes containing the serialized NameLookupSnapshot instance.\n    ///\n    /// # Raises\n    /// `ValueError` if serialization fails.\n    /// `RuntimeError` if the progress has already been resumed.\n    fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {\n        let bytes = crate::serialization::dump_lookup_snapshot(&self.snapshot, &self.script_name, &self.variable_name)?;\n        Ok(PyBytes::new(py, &bytes))\n    }\n\n    fn __repr__(&self) -> String {\n        format!(\n            \"NameLookupSnapshot(script_name='{}', variable_name={:?})\",\n            self.script_name, self.variable_name\n        )\n    }\n}\n\n/// Holds a `ResolveFutures` for either resource tracker variant.\n///\n/// Also holds REPL variants with `Py<PyMontyRepl>` for `MontyRepl.feed_start()`.\n///\n/// Used internally by `PyFutureSnapshot` to store execution state when\n/// awaiting resolution of pending async external calls.\n#[derive(Debug)]\npub(crate) enum EitherFutureSnapshot {\n    NoLimit(ResolveFutures<PySignalTracker<NoLimitTracker>>),\n    Limited(ResolveFutures<PySignalTracker<LimitedTracker>>),\n    ReplNoLimit(ReplResolveFutures<PySignalTracker<NoLimitTracker>>, Py<PyMontyRepl>),\n    ReplLimited(ReplResolveFutures<PySignalTracker<LimitedTracker>>, Py<PyMontyRepl>),\n    /// Sentinel indicating the snapshot has been consumed via `resume()`.\n    Done,\n}\n\n/// Helper trait for wrapping `ResolveFutures<T>` into `EitherFutureSnapshot`.\ntrait FromResolveFutures<T: ResourceTracker> {\n    /// Wraps a resolve-futures state into the appropriate variant.\n    fn from_resolve_futures(state: ResolveFutures<T>) -> Self;\n}\n\nimpl FromResolveFutures<PySignalTracker<NoLimitTracker>> for EitherFutureSnapshot {\n    fn from_resolve_futures(state: ResolveFutures<PySignalTracker<NoLimitTracker>>) -> Self {\n        Self::NoLimit(state)\n    }\n}\n\nimpl FromResolveFutures<PySignalTracker<LimitedTracker>> for EitherFutureSnapshot {\n    fn from_resolve_futures(state: ResolveFutures<PySignalTracker<LimitedTracker>>) -> Self {\n        Self::Limited(state)\n    }\n}\n\n/// Helper trait for wrapping `ReplResolveFutures<T>` into `EitherFutureSnapshot`.\ntrait FromReplResolveFutures<T: ResourceTracker> {\n    /// Wraps a REPL resolve-futures state into the appropriate variant.\n    fn from_repl_resolve_futures(state: ReplResolveFutures<T>, owner: Py<PyMontyRepl>) -> Self;\n}\n\nimpl FromReplResolveFutures<PySignalTracker<NoLimitTracker>> for EitherFutureSnapshot {\n    fn from_repl_resolve_futures(\n        state: ReplResolveFutures<PySignalTracker<NoLimitTracker>>,\n        owner: Py<PyMontyRepl>,\n    ) -> Self {\n        Self::ReplNoLimit(state, owner)\n    }\n}\n\nimpl FromReplResolveFutures<PySignalTracker<LimitedTracker>> for EitherFutureSnapshot {\n    fn from_repl_resolve_futures(\n        state: ReplResolveFutures<PySignalTracker<LimitedTracker>>,\n        owner: Py<PyMontyRepl>,\n    ) -> Self {\n        Self::ReplLimited(state, owner)\n    }\n}\n\n/// Snapshot generated during execution when monty yields to the host to resolve a future.\n///\n/// Works for both `Monty.start()` and `MontyRepl.feed_start()`.\n#[pyclass(name = \"FutureSnapshot\", module = \"pydantic_monty\", frozen)]\n#[derive(Debug)]\npub struct PyFutureSnapshot {\n    snapshot: Mutex<EitherFutureSnapshot>,\n    print_callback: Option<Py<PyAny>>,\n    dc_registry: DcRegistry,\n\n    /// Name of the script being executed\n    #[pyo3(get)]\n    pub script_name: String,\n}\n\nimpl PyFutureSnapshot {\n    fn new_py_any<T: ResourceTracker>(\n        py: Python<'_>,\n        state: ResolveFutures<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherFutureSnapshot: FromResolveFutures<T>,\n    {\n        let slf = Self {\n            snapshot: Mutex::new(EitherFutureSnapshot::from_resolve_futures(state)),\n            print_callback,\n            dc_registry,\n            script_name,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Constructs a `PyFutureSnapshot` from deserialized parts.\n    ///\n    /// Used by `load_snapshot` and `load_repl_snapshot` to reconstruct snapshot objects.\n    pub(crate) fn from_deserialized(\n        py: Python<'_>,\n        snapshot: EitherFutureSnapshot,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        script_name: String,\n    ) -> PyResult<Bound<'_, PyAny>> {\n        let slf = Self {\n            snapshot: Mutex::new(snapshot),\n            print_callback,\n            dc_registry,\n            script_name,\n        };\n        slf.into_bound_py_any(py)\n    }\n\n    /// Creates a `PyFutureSnapshot` for a REPL resolve-futures state.\n    fn repl_resolve_futures<T: ResourceTracker>(\n        py: Python<'_>,\n        state: ReplResolveFutures<T>,\n        script_name: String,\n        print_callback: Option<Py<PyAny>>,\n        dc_registry: DcRegistry,\n        repl_owner: Py<PyMontyRepl>,\n    ) -> PyResult<Bound<'_, PyAny>>\n    where\n        EitherFutureSnapshot: FromReplResolveFutures<T>,\n    {\n        let slf = Self {\n            snapshot: Mutex::new(EitherFutureSnapshot::from_repl_resolve_futures(state, repl_owner)),\n            print_callback,\n            dc_registry,\n            script_name,\n        };\n        slf.into_bound_py_any(py)\n    }\n}\n\n#[pymethods]\nimpl PyFutureSnapshot {\n    /// Resumes execution with results for one or more futures.\n    #[pyo3(signature = (results))]\n    pub fn resume<'py>(&self, py: Python<'py>, results: &Bound<'_, PyDict>) -> PyResult<Bound<'py, PyAny>> {\n        const ARGS_ERROR: &str = \"results values must be a dict with either 'return_value' or 'exception', not both\";\n\n        let mut snapshot = self\n            .snapshot\n            .lock()\n            .map_err(|_| PyRuntimeError::new_err(\"Snapshot is currently being resumed by another thread\"))?;\n\n        let snapshot = std::mem::replace(&mut *snapshot, EitherFutureSnapshot::Done);\n\n        let external_results = results\n            .iter()\n            .map(|(key, value)| {\n                let call_id = key.extract::<u32>()?;\n                let dict = value.cast::<PyDict>()?;\n                let value = extract_external_result(py, dict, ARGS_ERROR, &self.dc_registry, call_id)?;\n                Ok((call_id, value))\n            })\n            .collect::<PyResult<Vec<_>>>()?;\n\n        // Build print writer before detaching - clone_ref needs py token\n        let mut print_cb;\n        let print_writer = match &self.print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::from_py(cb.clone_ref(py));\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n        let mut print_writer = SendWrapper::new(print_writer);\n\n        let progress = match snapshot {\n            EitherFutureSnapshot::NoLimit(snapshot) => {\n                let result = py.detach(|| snapshot.resume(external_results, print_writer.reborrow()));\n                EitherProgress::NoLimit(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherFutureSnapshot::Limited(snapshot) => {\n                let result = py.detach(|| snapshot.resume(external_results, print_writer.reborrow()));\n                EitherProgress::Limited(result.map_err(|e| MontyError::new_err(py, e))?)\n            }\n            EitherFutureSnapshot::ReplNoLimit(snapshot, owner) => {\n                let result = py\n                    .detach(|| snapshot.resume(external_results, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplNoLimit(result, owner)\n            }\n            EitherFutureSnapshot::ReplLimited(snapshot, owner) => {\n                let result = py\n                    .detach(|| snapshot.resume(external_results, print_writer.reborrow()))\n                    .map_err(|e| restore_repl_from_repl_start_error(py, &owner, *e))?;\n                EitherProgress::ReplLimited(result, owner)\n            }\n            EitherFutureSnapshot::Done => return Err(PyRuntimeError::new_err(\"Progress already resumed\")),\n        };\n\n        // Clone the Arc handle for the next snapshot/complete\n        let dc_registry = self.dc_registry.clone_ref(py);\n        progress.progress_or_complete(\n            py,\n            self.script_name.clone(),\n            self.print_callback.as_ref().map(|cb| cb.clone_ref(py)),\n            dc_registry,\n        )\n    }\n\n    /// Returns the pending call IDs associated with the FutureSnapshot instance.\n    ///\n    /// # Returns\n    /// A slice of pending call IDs.\n    #[getter]\n    fn pending_call_ids<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {\n        let snapshot = self.snapshot.lock().unwrap_or_else(PoisonError::into_inner);\n        match &*snapshot {\n            EitherFutureSnapshot::NoLimit(snapshot) => PyList::new(py, snapshot.pending_call_ids()),\n            EitherFutureSnapshot::Limited(snapshot) => PyList::new(py, snapshot.pending_call_ids()),\n            EitherFutureSnapshot::ReplNoLimit(snapshot, _) => PyList::new(py, snapshot.pending_call_ids()),\n            EitherFutureSnapshot::ReplLimited(snapshot, _) => PyList::new(py, snapshot.pending_call_ids()),\n            EitherFutureSnapshot::Done => Err(PyRuntimeError::new_err(\"FutureSnapshot already resumed\")),\n        }\n    }\n\n    /// Serializes the FutureSnapshot instance to a binary format.\n    ///\n    /// The serialized data can be stored and later restored with `load_snapshot()`\n    /// or `load_repl_snapshot()`. REPL snapshots automatically include the REPL state.\n    ///\n    /// Note: The `print_callback` is not serialized and must be re-provided when loading.\n    ///\n    /// # Returns\n    /// Bytes containing the serialized FutureSnapshot instance.\n    ///\n    /// # Raises\n    /// `ValueError` if serialization fails.\n    /// `RuntimeError` if the progress has already been resumed.\n    fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {\n        let bytes = crate::serialization::dump_future_snapshot(&self.snapshot, &self.script_name)?;\n        Ok(PyBytes::new(py, &bytes))\n    }\n\n    fn __repr__(&self) -> String {\n        let snapshot = self.snapshot.lock().unwrap_or_else(PoisonError::into_inner);\n        let pending_call_ids = match &*snapshot {\n            EitherFutureSnapshot::NoLimit(s) => s.pending_call_ids(),\n            EitherFutureSnapshot::Limited(s) => s.pending_call_ids(),\n            EitherFutureSnapshot::ReplNoLimit(s, _) => s.pending_call_ids(),\n            EitherFutureSnapshot::ReplLimited(s, _) => s.pending_call_ids(),\n            EitherFutureSnapshot::Done => &[],\n        };\n        format!(\n            \"FutureSnapshot(script_name='{}', pending_call_ids={pending_call_ids:?})\",\n            self.script_name,\n        )\n    }\n}\n\n#[pyclass(name = \"MontyComplete\", module = \"pydantic_monty\", frozen)]\npub struct PyMontyComplete {\n    #[pyo3(get)]\n    pub output: Py<PyAny>,\n    // TODO we might want to add stats on execution here like time, allocations, etc.\n}\n\nimpl PyMontyComplete {\n    fn create<'py>(py: Python<'py>, output: &MontyObject, dc_registry: &DcRegistry) -> PyResult<Bound<'py, PyAny>> {\n        let output = monty_to_py(py, output, dc_registry)?;\n        let slf = Self { output };\n        slf.into_bound_py_any(py)\n    }\n}\n\n#[pymethods]\nimpl PyMontyComplete {\n    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {\n        Ok(format!(\"MontyComplete(output={})\", self.output.bind(py).repr()?))\n    }\n}\n\nfn list_str(arg: Option<&Bound<'_, PyList>>, name: &str) -> PyResult<Vec<String>> {\n    if let Some(names) = arg {\n        names\n            .iter()\n            .map(|item| item.extract::<String>())\n            .collect::<PyResult<Vec<_>>>()\n            .map_err(|e| PyTypeError::new_err(format!(\"{name}: {e}\")))\n    } else {\n        Ok(vec![])\n    }\n}\n\n/// A `PrintWriter` implementation that calls a Python callback for each print output.\n///\n/// This struct holds a GIL-independent `Py<PyAny>` reference to the callback,\n/// allowing it to be used across GIL release boundaries. The GIL is re-acquired\n/// briefly for each callback invocation.\n#[derive(Debug)]\npub(crate) struct CallbackStringPrint(Py<PyAny>);\n\nimpl CallbackStringPrint {\n    /// Creates a new `CallbackStringPrint` from a borrowed Python callback.\n    fn new(callback: &Bound<'_, PyAny>) -> Self {\n        Self(callback.clone().unbind())\n    }\n\n    /// Creates a new `CallbackStringPrint` from an owned `Py<PyAny>`.\n    pub(crate) fn from_py(callback: Py<PyAny>) -> Self {\n        Self(callback)\n    }\n}\n\nimpl PrintWriterCallback for CallbackStringPrint {\n    fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException> {\n        Python::attach(|py| {\n            self.0.bind(py).call1((\"stdout\", output.as_ref()))?;\n            Ok::<_, PyErr>(())\n        })\n        .map_err(|e| Python::attach(|py| exc_py_to_monty(py, &e)))\n    }\n\n    fn stdout_push(&mut self, end: char) -> Result<(), MontyException> {\n        Python::attach(|py| {\n            self.0.bind(py).call1((\"stdout\", end.to_string()))?;\n            Ok::<_, PyErr>(())\n        })\n        .map_err(|e| Python::attach(|py| exc_py_to_monty(py, &e)))\n    }\n}\n\n/// Recursively checks whether a `MontyObject` contains a dataclass, including\n/// inside containers like `List`, `Tuple`, and `Dict`.\n///\n/// This is used to decide whether to take the iterative execution path: dataclass\n/// method calls need host dispatch, so if any input (even nested) is a dataclass\n/// we must use the iterative runner rather than the non-iterative `run()`.\nfn contains_dataclass(obj: &MontyObject) -> bool {\n    match obj {\n        MontyObject::Dataclass { .. } => true,\n        MontyObject::List(items) | MontyObject::Tuple(items) => items.iter().any(contains_dataclass),\n        MontyObject::Dict(pairs) => pairs\n            .into_iter()\n            .any(|(k, v)| contains_dataclass(k) || contains_dataclass(v)),\n        _ => false,\n    }\n}\n\n/// Serialization wrapper for `PyMonty` that includes all fields needed for reconstruction.\n#[derive(serde::Serialize, serde::Deserialize)]\nstruct SerializedMonty {\n    runner: MontyRun,\n    script_name: String,\n    input_names: Vec<String>,\n}\n\n/// Extract an external result (object or exception) from a dictionary.\n///\n/// Any dataclass return values are automatically registered in the `dc_registry` via `py_to_monty`\n/// so they can be properly reconstructed on output.\n/// Extracts an `ExternalResult` from a Python dict with a single key.\n///\n/// Accepts `return_value`, `exception`, or `future` (with value `...`).\n/// The `call_id` is required for `future` results to track the pending call.\nfn extract_external_result(\n    py: Python<'_>,\n    dict: &Bound<'_, PyDict>,\n    error_msg: &'static str,\n    dc_registry: &DcRegistry,\n    call_id: u32,\n) -> PyResult<ExtFunctionResult> {\n    if dict.len() != 1 {\n        Err(PyTypeError::new_err(error_msg))\n    } else if let Some(rv) = dict.get_item(intern!(py, \"return_value\"))? {\n        // Return value provided\n        Ok(py_to_monty(&rv, dc_registry)?.into())\n    } else if let Some(exc) = dict.get_item(intern!(py, \"exception\"))? {\n        // Exception provided\n        let py_err = PyErr::from_value(exc.into_any());\n        Ok(exc_py_to_monty(py, &py_err).into())\n    } else if let Some(exc) = dict.get_item(intern!(py, \"future\"))? {\n        if exc.eq(py.Ellipsis()).unwrap_or_default() {\n            Ok(ExtFunctionResult::Future(call_id))\n        } else {\n            Err(PyTypeError::new_err(\n                \"Value for the 'future' key must be Ellipsis (...)\",\n            ))\n        }\n    } else {\n        // wrong key in kwargs\n        Err(PyTypeError::new_err(error_msg))\n    }\n}\n\n/// Extracts the REPL from a `ReplStartError`, restores it into the owner,\n/// and returns the Python exception.\nfn restore_repl_from_repl_start_error<T: ResourceTracker>(\n    py: Python<'_>,\n    repl_owner: &Py<PyMontyRepl>,\n    err: ReplStartError<T>,\n) -> PyErr\nwhere\n    EitherRepl: FromCoreRepl<T>,\n{\n    repl_owner.get().put_repl(EitherRepl::from_core(err.repl));\n    MontyError::new_err(py, err.error)\n}\n"
  },
  {
    "path": "crates/monty-python/src/repl.rs",
    "content": "use std::sync::{Mutex, PoisonError};\n\n// Use `::monty` to refer to the external crate (not the pymodule)\nuse ::monty::{\n    ExtFunctionResult, LimitedTracker, MontyException, MontyObject, MontyRepl as CoreMontyRepl, NameLookupResult,\n    NoLimitTracker, PrintWriter, ReplProgress, ReplStartError, ResourceTracker,\n};\nuse monty::ExcType;\nuse pyo3::{\n    exceptions::{PyRuntimeError, PyValueError},\n    prelude::*,\n    types::{PyBytes, PyDict, PyList, PyTuple},\n};\nuse send_wrapper::SendWrapper;\n\nuse crate::{\n    convert::{get_docstring, monty_to_py, py_to_monty},\n    dataclass::DcRegistry,\n    exceptions::{MontyError, exc_py_to_monty},\n    external::{ExternalFunctionRegistry, dispatch_method_call},\n    limits::{PySignalTracker, extract_limits},\n    monty_cls::CallbackStringPrint,\n};\n\n/// Runtime REPL session holder for pyclass interoperability.\n///\n/// PyO3 classes cannot be generic, so this enum stores REPL sessions for both\n/// resource tracker variants.\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub(crate) enum EitherRepl {\n    NoLimit(CoreMontyRepl<PySignalTracker<NoLimitTracker>>),\n    Limited(CoreMontyRepl<PySignalTracker<LimitedTracker>>),\n}\n\n/// Stateful no-replay REPL session.\n///\n/// Create with `MontyRepl()` then call `feed_run()` to execute snippets\n/// incrementally against persistent heap and namespace state.\n///\n/// Uses `Mutex` for the inner REPL because `CoreMontyRepl` contains a `Heap`\n/// with `Cell<usize>` (not `Sync`), and PyO3 requires `Send + Sync` for all\n/// pyclass types. The mutex also prevents concurrent `feed_run()` calls.\n#[pyclass(name = \"MontyRepl\", module = \"pydantic_monty\", frozen)]\n#[derive(Debug)]\npub struct PyMontyRepl {\n    repl: Mutex<Option<EitherRepl>>,\n    dc_registry: DcRegistry,\n\n    /// Name of the script being executed.\n    #[pyo3(get)]\n    pub script_name: String,\n}\n\n#[pymethods]\nimpl PyMontyRepl {\n    /// Creates an empty REPL session ready to receive snippets via `feed_run()`.\n    ///\n    /// No code is parsed or executed at construction time — all execution\n    /// is driven through `feed_run()`.\n    #[new]\n    #[pyo3(signature = (*, script_name=\"main.py\", limits=None, dataclass_registry=None))]\n    fn new(\n        py: Python<'_>,\n        script_name: &str,\n        limits: Option<&Bound<'_, PyDict>>,\n        dataclass_registry: Option<&Bound<'_, PyList>>,\n    ) -> PyResult<Self> {\n        let dc_registry = DcRegistry::from_list(py, dataclass_registry)?;\n        let script_name = script_name.to_string();\n\n        let repl = if let Some(limits) = limits {\n            let tracker = PySignalTracker::new(LimitedTracker::new(extract_limits(limits)?));\n            EitherRepl::Limited(CoreMontyRepl::new(&script_name, tracker))\n        } else {\n            let tracker = PySignalTracker::new(NoLimitTracker);\n            EitherRepl::NoLimit(CoreMontyRepl::new(&script_name, tracker))\n        };\n\n        Ok(Self {\n            repl: Mutex::new(Some(repl)),\n            dc_registry,\n            script_name,\n        })\n    }\n\n    /// Registers a dataclass type for proper isinstance() support on output.\n    fn register_dataclass(&self, cls: &Bound<'_, pyo3::types::PyType>) -> PyResult<()> {\n        self.dc_registry.insert(cls)\n    }\n\n    /// Feeds and executes a single incremental REPL snippet.\n    ///\n    /// The snippet is compiled against existing session state and executed once\n    /// without replaying previously fed snippets.\n    ///\n    /// When `external_functions` is provided, external function calls and name\n    /// lookups are dispatched to the provided callables — matching the behavior\n    /// of `Monty.run(external_functions=...)`.\n    #[pyo3(signature = (code, *, inputs=None, external_functions=None, print_callback=None, os=None))]\n    fn feed_run<'py>(\n        &self,\n        py: Python<'py>,\n        code: &str,\n        inputs: Option<&Bound<'_, PyDict>>,\n        external_functions: Option<&Bound<'_, PyDict>>,\n        print_callback: Option<Py<PyAny>>,\n        os: Option<&Bound<'_, PyAny>>,\n    ) -> PyResult<Bound<'py, PyAny>> {\n        let input_values = extract_repl_inputs(inputs, &self.dc_registry)?;\n\n        let mut print_cb;\n        let mut print_writer = match print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::from_py(cb);\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n\n        if external_functions.is_some() || os.is_some() {\n            return self.feed_run_with_externals(py, code, input_values, external_functions, os, print_writer);\n        }\n\n        let mut guard = self\n            .repl\n            .try_lock()\n            .map_err(|_| PyRuntimeError::new_err(\"REPL session is currently executing another snippet\"))?;\n        let repl = guard\n            .as_mut()\n            .ok_or_else(|| PyRuntimeError::new_err(\"REPL session is currently executing another snippet\"))?;\n\n        let output = match repl {\n            EitherRepl::NoLimit(repl) => repl.feed_run(code, input_values, print_writer.reborrow()),\n            EitherRepl::Limited(repl) => repl.feed_run(code, input_values, print_writer.reborrow()),\n        }\n        .map_err(|e| MontyError::new_err(py, e))?;\n\n        Ok(monty_to_py(py, &output, &self.dc_registry)?.into_bound(py))\n    }\n\n    /// Starts executing an incremental snippet, yielding snapshots for external calls.\n    ///\n    /// Unlike `feed_run()`, which handles external function dispatch internally via a loop,\n    /// `feed_start()` returns a snapshot object whenever the code needs an external function\n    /// call, OS call, name lookup, or future resolution. The caller then provides the result\n    /// via `snapshot.resume(...)`, which returns the next snapshot or `MontyComplete`.\n    ///\n    /// This enables the same iterative start/resume pattern used by `Monty.start()`,\n    /// including support for async external functions via `FutureSnapshot`.\n    #[pyo3(signature = (code, *, inputs=None, print_callback=None))]\n    fn feed_start<'py>(\n        slf: &Bound<'py, Self>,\n        py: Python<'py>,\n        code: &str,\n        inputs: Option<&Bound<'_, PyDict>>,\n        print_callback: Option<Py<PyAny>>,\n    ) -> PyResult<Bound<'py, PyAny>> {\n        let this = slf.get();\n        let input_values = extract_repl_inputs(inputs, &this.dc_registry)?;\n\n        let mut print_cb;\n        let print_writer = match &print_callback {\n            Some(cb) => {\n                print_cb = CallbackStringPrint::from_py(cb.clone_ref(py));\n                PrintWriter::Callback(&mut print_cb)\n            }\n            None => PrintWriter::Stdout,\n        };\n        let mut print_output = SendWrapper::new(print_writer);\n\n        let repl = this.take_repl()?;\n        let repl_owner: Py<Self> = slf.clone().unbind();\n\n        let code_owned = code.to_owned();\n        let inputs_owned = input_values;\n        let dc_registry = this.dc_registry.clone_ref(py);\n        let script_name = this.script_name.clone();\n\n        match repl {\n            EitherRepl::NoLimit(repl) => {\n                let progress = py\n                    .detach(|| repl.feed_start(&code_owned, inputs_owned, print_output.reborrow()))\n                    .map_err(|e| this.restore_repl_from_start_error(py, *e))?;\n                let either = crate::monty_cls::EitherProgress::ReplNoLimit(progress, repl_owner);\n                either.progress_or_complete(py, script_name, print_callback, dc_registry)\n            }\n            EitherRepl::Limited(repl) => {\n                let progress = py\n                    .detach(|| repl.feed_start(&code_owned, inputs_owned, print_output.reborrow()))\n                    .map_err(|e| this.restore_repl_from_start_error(py, *e))?;\n                let either = crate::monty_cls::EitherProgress::ReplLimited(progress, repl_owner);\n                either.progress_or_complete(py, script_name, print_callback, dc_registry)\n            }\n        }\n    }\n\n    /// Serializes this REPL session to bytes.\n    fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {\n        #[derive(serde::Serialize)]\n        struct SerializedRepl<'a> {\n            repl: &'a EitherRepl,\n            script_name: &'a str,\n        }\n\n        let guard = self.repl.lock().unwrap_or_else(PoisonError::into_inner);\n        let repl = guard\n            .as_ref()\n            .ok_or_else(|| PyRuntimeError::new_err(\"REPL session is currently executing another snippet\"))?;\n\n        let serialized = SerializedRepl {\n            repl,\n            script_name: &self.script_name,\n        };\n        let bytes = postcard::to_allocvec(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))?;\n        Ok(PyBytes::new(py, &bytes))\n    }\n\n    /// Restores a REPL session from `dump()` bytes.\n    #[staticmethod]\n    #[pyo3(signature = (data, *, dataclass_registry=None))]\n    fn load(\n        py: Python<'_>,\n        data: &Bound<'_, PyBytes>,\n        dataclass_registry: Option<&Bound<'_, PyList>>,\n    ) -> PyResult<Self> {\n        #[derive(serde::Deserialize)]\n        struct SerializedReplOwned {\n            repl: EitherRepl,\n            script_name: String,\n        }\n\n        let serialized: SerializedReplOwned =\n            postcard::from_bytes(data.as_bytes()).map_err(|e| PyValueError::new_err(e.to_string()))?;\n\n        Ok(Self {\n            repl: Mutex::new(Some(serialized.repl)),\n            dc_registry: DcRegistry::from_list(py, dataclass_registry)?,\n            script_name: serialized.script_name,\n        })\n    }\n\n    fn __repr__(&self) -> String {\n        format!(\"MontyRepl(script_name='{}')\", self.script_name)\n    }\n}\n\nimpl PyMontyRepl {\n    /// Executes a REPL snippet with external function and OS call support.\n    ///\n    /// Uses the iterative `feed_start` / resume loop to handle external function\n    /// calls and name lookups, matching the same dispatch logic as `Monty.run()`.\n    ///\n    /// `feed_start` consumes the REPL, so we temporarily take it out of the mutex\n    /// (leaving `None`) and restore it on both success and error paths.\n    fn feed_run_with_externals<'py>(\n        &self,\n        py: Python<'py>,\n        code: &str,\n        input_values: Vec<(String, MontyObject)>,\n        external_functions: Option<&Bound<'_, PyDict>>,\n        os: Option<&Bound<'_, PyAny>>,\n        mut print_writer: PrintWriter<'_>,\n    ) -> PyResult<Bound<'py, PyAny>> {\n        let mut print_output = SendWrapper::new(&mut print_writer);\n\n        let repl = self.take_repl()?;\n\n        let result = match repl {\n            EitherRepl::NoLimit(repl) => {\n                self.feed_start_loop(py, repl, code, input_values, external_functions, os, &mut print_output)\n            }\n            EitherRepl::Limited(repl) => {\n                self.feed_start_loop(py, repl, code, input_values, external_functions, os, &mut print_output)\n            }\n        };\n\n        // On error, the REPL is already restored inside `restore_repl_from_start_error`.\n        match result {\n            Ok((output, restored_repl)) => {\n                self.put_repl(restored_repl);\n                Ok(monty_to_py(py, &output, &self.dc_registry)?.into_bound(py))\n            }\n            Err(err) => Err(err),\n        }\n    }\n\n    /// Runs the feed_start / resume loop for a specific resource tracker type.\n    ///\n    /// Returns the output value and the restored REPL enum variant, or a Python error.\n    #[expect(clippy::too_many_arguments)]\n    fn feed_start_loop<T: ResourceTracker + Send>(\n        &self,\n        py: Python<'_>,\n        repl: CoreMontyRepl<T>,\n        code: &str,\n        input_values: Vec<(String, MontyObject)>,\n        external_functions: Option<&Bound<'_, PyDict>>,\n        os: Option<&Bound<'_, PyAny>>,\n        print_output: &mut SendWrapper<&mut PrintWriter<'_>>,\n    ) -> PyResult<(MontyObject, EitherRepl)>\n    where\n        EitherRepl: FromCoreRepl<T>,\n    {\n        let code_owned = code.to_owned();\n        let mut progress = py\n            .detach(|| repl.feed_start(&code_owned, input_values, print_output.reborrow()))\n            .map_err(|e| self.restore_repl_from_start_error(py, *e))?;\n\n        loop {\n            match progress {\n                ReplProgress::Complete { repl, value } => {\n                    return Ok((value, EitherRepl::from_core(repl)));\n                }\n                ReplProgress::FunctionCall(call) => {\n                    let return_value = if call.method_call {\n                        dispatch_method_call(py, &call.function_name, &call.args, &call.kwargs, &self.dc_registry)\n                    } else if let Some(ext_fns) = external_functions {\n                        let registry = ExternalFunctionRegistry::new(py, ext_fns, &self.dc_registry);\n                        registry.call(&call.function_name, &call.args, &call.kwargs)\n                    } else {\n                        let msg = format!(\n                            \"External function '{}' called but no external_functions provided\",\n                            call.function_name\n                        );\n                        self.put_repl(EitherRepl::from_core(call.into_repl()));\n                        return Err(PyRuntimeError::new_err(msg));\n                    };\n\n                    progress = py\n                        .detach(|| call.resume(return_value, print_output.reborrow()))\n                        .map_err(|e| self.restore_repl_from_start_error(py, *e))?;\n                }\n                ReplProgress::NameLookup(lookup) => {\n                    let result = if let Some(ext_fns) = external_functions\n                        && let Some(value) = ext_fns.get_item(&lookup.name)?\n                    {\n                        NameLookupResult::Value(MontyObject::Function {\n                            name: lookup.name.clone(),\n                            docstring: get_docstring(&value),\n                        })\n                    } else {\n                        NameLookupResult::Undefined\n                    };\n\n                    progress = py\n                        .detach(|| lookup.resume(result, print_output.reborrow()))\n                        .map_err(|e| self.restore_repl_from_start_error(py, *e))?;\n                }\n                ReplProgress::OsCall(call) => {\n                    let result: ExtFunctionResult = if let Some(os_callback) = os {\n                        let py_args: Vec<Py<PyAny>> = call\n                            .args\n                            .iter()\n                            .map(|arg| monty_to_py(py, arg, &self.dc_registry))\n                            .collect::<PyResult<_>>()?;\n                        let py_args_tuple = PyTuple::new(py, py_args)?;\n\n                        let py_kwargs = PyDict::new(py);\n                        for (k, v) in &call.kwargs {\n                            py_kwargs.set_item(\n                                monty_to_py(py, k, &self.dc_registry)?,\n                                monty_to_py(py, v, &self.dc_registry)?,\n                            )?;\n                        }\n\n                        match os_callback.call1((call.function.to_string(), py_args_tuple, py_kwargs)) {\n                            Ok(result) => py_to_monty(&result, &self.dc_registry)?.into(),\n                            Err(err) => exc_py_to_monty(py, &err).into(),\n                        }\n                    } else {\n                        MontyException::new(\n                            ExcType::NotImplementedError,\n                            Some(format!(\"OS function '{}' not implemented\", call.function)),\n                        )\n                        .into()\n                    };\n\n                    progress = py\n                        .detach(|| call.resume(result, print_output.reborrow()))\n                        .map_err(|e| self.restore_repl_from_start_error(py, *e))?;\n                }\n                ReplProgress::ResolveFutures(state) => {\n                    self.put_repl(EitherRepl::from_core(state.into_repl()));\n                    return Err(PyRuntimeError::new_err(\n                        \"async futures not supported with `MontyRepl.feed_run`\",\n                    ));\n                }\n            }\n        }\n    }\n\n    /// Takes the REPL out of the mutex for `feed_start` (which consumes self),\n    /// leaving `None` until the REPL is restored via `put_repl`.\n    pub(crate) fn take_repl(&self) -> PyResult<EitherRepl> {\n        let mut guard = self\n            .repl\n            .try_lock()\n            .map_err(|_| PyRuntimeError::new_err(\"REPL session is currently executing another snippet\"))?;\n        guard\n            .take()\n            .ok_or_else(|| PyRuntimeError::new_err(\"REPL session is currently executing another snippet\"))\n    }\n\n    /// Creates an empty REPL owner for snapshot deserialization.\n    ///\n    /// The REPL mutex starts as `None` — the real REPL state lives inside the\n    /// deserialized snapshot and will be restored via `put_repl` when the\n    /// snapshot is resumed to completion.\n    pub(crate) fn empty_owner(script_name: String, dc_registry: DcRegistry) -> Self {\n        Self {\n            repl: Mutex::new(None),\n            dc_registry,\n            script_name,\n        }\n    }\n\n    /// Restores a REPL into the mutex after `feed_start` completes successfully.\n    pub(crate) fn put_repl(&self, repl: EitherRepl) {\n        let mut guard = self.repl.lock().unwrap_or_else(PoisonError::into_inner);\n        *guard = Some(repl);\n    }\n\n    /// Extracts the REPL from a `ReplStartError`, restores it into `self.repl`,\n    /// and returns the Python exception.\n    fn restore_repl_from_start_error<T: ResourceTracker>(&self, py: Python<'_>, err: ReplStartError<T>) -> PyErr\n    where\n        EitherRepl: FromCoreRepl<T>,\n    {\n        self.put_repl(EitherRepl::from_core(err.repl));\n        MontyError::new_err(py, err.error)\n    }\n}\n\n/// Converts a Python dict of `{name: value}` pairs into the `Vec<(String, MontyObject)>`\n/// format expected by the core REPL's `feed_run` and `feed_start`.\nfn extract_repl_inputs(\n    inputs: Option<&Bound<'_, PyDict>>,\n    dc_registry: &DcRegistry,\n) -> PyResult<Vec<(String, MontyObject)>> {\n    let Some(inputs) = inputs else {\n        return Ok(vec![]);\n    };\n    inputs\n        .iter()\n        .map(|(key, value)| {\n            let name = key.extract::<String>()?;\n            let obj = py_to_monty(&value, dc_registry)?;\n            Ok((name, obj))\n        })\n        .collect::<PyResult<_>>()\n}\n\n/// Helper trait to convert a typed `CoreMontyRepl<T>` back into the\n/// type-erased `EitherRepl` enum.\npub(crate) trait FromCoreRepl<T: ResourceTracker> {\n    /// Wraps a core REPL into the appropriate `EitherRepl` variant.\n    fn from_core(repl: CoreMontyRepl<T>) -> Self;\n}\n\nimpl FromCoreRepl<PySignalTracker<NoLimitTracker>> for EitherRepl {\n    fn from_core(repl: CoreMontyRepl<PySignalTracker<NoLimitTracker>>) -> Self {\n        Self::NoLimit(repl)\n    }\n}\n\nimpl FromCoreRepl<PySignalTracker<LimitedTracker>> for EitherRepl {\n    fn from_core(repl: CoreMontyRepl<PySignalTracker<LimitedTracker>>) -> Self {\n        Self::Limited(repl)\n    }\n}\n"
  },
  {
    "path": "crates/monty-python/src/serialization.rs",
    "content": "//! Unified snapshot serialization with versioning and integrity checks.\n//!\n//! All snapshot `dump()` calls produce a wire format:\n//!\n//! ```text\n//! [version: u16 LE] [sha256: 32 bytes] [postcard payload]\n//! ```\n//!\n//! Two module-level `#[pyfunction]`s — `load_snapshot` and `load_repl_snapshot` —\n//! handle deserialization without requiring callers to know the snapshot type.\n\nuse std::sync::{Mutex, PoisonError};\n\nuse ::monty::{\n    FunctionCall, LimitedTracker, MontyObject, NameLookup, NoLimitTracker, OsCall, ReplFunctionCall, ReplNameLookup,\n    ReplOsCall, ReplResolveFutures, ResolveFutures,\n};\nuse pyo3::{\n    exceptions::{PyRuntimeError, PyValueError},\n    prelude::*,\n    types::{PyBytes, PyDict, PyList, PyTuple},\n};\nuse serde::{Deserialize, Serialize};\nuse sha2::{Digest, Sha256};\n\nuse crate::{\n    convert::{monty_to_py, py_to_monty},\n    dataclass::DcRegistry,\n    limits::PySignalTracker,\n    monty_cls::{\n        EitherFunctionSnapshot, EitherFutureSnapshot, EitherLookupSnapshot, PyFunctionSnapshot, PyFutureSnapshot,\n        PyNameLookupSnapshot,\n    },\n    repl::PyMontyRepl,\n};\n\n/// Current serialization format version. Incremented on breaking wire-format changes.\nconst SERIALIZATION_VERSION: u16 = 1;\n\n/// Size of the wire-format header: 2 bytes version + 32 bytes SHA-256 hash.\nconst HEADER_SIZE: usize = 2 + 32;\n\n// ---------------------------------------------------------------------------\n// Wire-format helpers\n// ---------------------------------------------------------------------------\n\n/// Serializes a value with a version header and SHA-256 integrity hash.\n///\n/// Layout: `[version: u16 LE] [sha256(payload): 32 bytes] [postcard payload]`\nfn serialize_with_header(value: &impl Serialize) -> Result<Vec<u8>, postcard::Error> {\n    let payload = postcard::to_allocvec(value)?;\n\n    let hash = Sha256::digest(&payload);\n\n    let mut buf = Vec::with_capacity(HEADER_SIZE + payload.len());\n    buf.extend_from_slice(&SERIALIZATION_VERSION.to_le_bytes());\n    buf.extend_from_slice(&hash);\n    buf.extend_from_slice(&payload);\n    Ok(buf)\n}\n\n/// Deserializes bytes produced by `serialize_with_header`, checking version and integrity.\nfn deserialize_with_header<'de, T: Deserialize<'de>>(bytes: &'de [u8]) -> PyResult<T> {\n    if bytes.len() < HEADER_SIZE {\n        return Err(PyValueError::new_err(\n            \"Serialized data is too short to contain a valid header\",\n        ));\n    }\n\n    let version = u16::from_le_bytes([bytes[0], bytes[1]]);\n    if version != SERIALIZATION_VERSION {\n        return Err(PyValueError::new_err(format!(\n            \"Serialized data version {version} is not compatible with current version {SERIALIZATION_VERSION}\"\n        )));\n    }\n\n    let stored_hash = &bytes[2..HEADER_SIZE];\n    let payload = &bytes[HEADER_SIZE..];\n\n    let computed_hash = Sha256::digest(payload);\n    if computed_hash.as_slice() != stored_hash {\n        return Err(PyValueError::new_err(\"Serialized data integrity check failed\"));\n    }\n\n    postcard::from_bytes(payload).map_err(|e| PyValueError::new_err(e.to_string()))\n}\n\n// ---------------------------------------------------------------------------\n// Tagged wrapper enums\n// ---------------------------------------------------------------------------\n\n/// Non-REPL snapshot: tagged union over all snapshot types.\n///\n/// Postcard's enum tagging handles type discrimination, so `load_snapshot`\n/// doesn't need to know the snapshot type upfront.\n///\n/// Uses `Serde*Snapshot` types for snapshot fields — these are the wire-format\n/// representations without `Py<PyMontyRepl>` references.\n#[derive(Serialize, Deserialize)]\npub(crate) enum SerializedSnapshot {\n    /// External function or OS call.\n    Function {\n        snapshot: SerdeFunctionSnapshot,\n        script_name: String,\n        is_os_function: bool,\n        is_method_call: bool,\n        function_name: String,\n        args: Vec<MontyObject>,\n        kwargs: Vec<(MontyObject, MontyObject)>,\n        call_id: u32,\n    },\n    /// Name lookup.\n    NameLookup {\n        snapshot: SerdeLookupSnapshot,\n        script_name: String,\n        variable_name: String,\n    },\n    /// Future resolution.\n    Future {\n        snapshot: SerdeFutureSnapshot,\n        script_name: String,\n    },\n}\n\n/// REPL snapshot: includes the REPL state alongside the execution snapshot.\n///\n/// On deserialization, the REPL state is reconstructed into a fresh `PyMontyRepl`\n/// and the snapshot is rewired to reference it.\n///\n/// Uses `SerdeFunctionSnapshot` (etc.) directly so REPL call variants are preserved\n/// in the wire format — unlike `EitherFunctionSnapshot::Deserialize` which maps\n/// REPL variants to `Done`.\n#[derive(Serialize, Deserialize)]\npub(crate) enum SerializedReplSnapshot {\n    /// External function or OS call with REPL state.\n    ///\n    /// The REPL state is embedded inside the snapshot's `Repl*` variant — no\n    /// separate `repl` field is needed.\n    Function {\n        snapshot: SerdeFunctionSnapshot,\n        script_name: String,\n        is_os_function: bool,\n        is_method_call: bool,\n        function_name: String,\n        args: Vec<MontyObject>,\n        kwargs: Vec<(MontyObject, MontyObject)>,\n        call_id: u32,\n    },\n    /// Name lookup with REPL state.\n    NameLookup {\n        snapshot: SerdeLookupSnapshot,\n        script_name: String,\n        variable_name: String,\n    },\n    /// Future resolution with REPL state.\n    Future {\n        snapshot: SerdeFutureSnapshot,\n        script_name: String,\n    },\n}\n\n// ---------------------------------------------------------------------------\n// Serde helpers for Either*Snapshot types\n// ---------------------------------------------------------------------------\n\n/// Wire-format representation of `EitherFunctionSnapshot` without `Py<PyMontyRepl>`.\n///\n/// REPL variants preserve the inner call data for round-tripping through\n/// `load_repl_snapshot`. Non-REPL variants pass through directly.\n#[derive(Serialize, Deserialize)]\npub(crate) enum SerdeFunctionSnapshot {\n    NoLimitFn(FunctionCall<PySignalTracker<NoLimitTracker>>),\n    NoLimitOs(OsCall<PySignalTracker<NoLimitTracker>>),\n    LimitedFn(FunctionCall<PySignalTracker<LimitedTracker>>),\n    LimitedOs(OsCall<PySignalTracker<LimitedTracker>>),\n    ReplNoLimitFn(ReplFunctionCall<PySignalTracker<NoLimitTracker>>),\n    ReplNoLimitOs(ReplOsCall<PySignalTracker<NoLimitTracker>>),\n    ReplLimitedFn(ReplFunctionCall<PySignalTracker<LimitedTracker>>),\n    ReplLimitedOs(ReplOsCall<PySignalTracker<LimitedTracker>>),\n    Done,\n}\n\n/// Borrowing version of `SerdeFunctionSnapshot` for zero-copy serialization.\n#[derive(Serialize)]\nenum SerdeFunctionSnapshotRef<'a> {\n    NoLimitFn(&'a FunctionCall<PySignalTracker<NoLimitTracker>>),\n    NoLimitOs(&'a OsCall<PySignalTracker<NoLimitTracker>>),\n    LimitedFn(&'a FunctionCall<PySignalTracker<LimitedTracker>>),\n    LimitedOs(&'a OsCall<PySignalTracker<LimitedTracker>>),\n    ReplNoLimitFn(&'a ReplFunctionCall<PySignalTracker<NoLimitTracker>>),\n    ReplNoLimitOs(&'a ReplOsCall<PySignalTracker<NoLimitTracker>>),\n    ReplLimitedFn(&'a ReplFunctionCall<PySignalTracker<LimitedTracker>>),\n    ReplLimitedOs(&'a ReplOsCall<PySignalTracker<LimitedTracker>>),\n    Done,\n}\n\nimpl SerdeFunctionSnapshot {\n    /// Converts into `EitherFunctionSnapshot` for the non-REPL path.\n    ///\n    /// Returns an error if this contains a REPL variant — use `into_either_with_repl`\n    /// for REPL snapshots instead.\n    fn into_either(self) -> PyResult<EitherFunctionSnapshot> {\n        match self {\n            Self::NoLimitFn(c) => Ok(EitherFunctionSnapshot::NoLimitFn(c)),\n            Self::NoLimitOs(c) => Ok(EitherFunctionSnapshot::NoLimitOs(c)),\n            Self::LimitedFn(c) => Ok(EitherFunctionSnapshot::LimitedFn(c)),\n            Self::LimitedOs(c) => Ok(EitherFunctionSnapshot::LimitedOs(c)),\n            Self::ReplNoLimitFn(_) | Self::ReplNoLimitOs(_) | Self::ReplLimitedFn(_) | Self::ReplLimitedOs(_) => Err(\n                PyValueError::new_err(\"Cannot load a REPL snapshot with load_snapshot, use load_repl_snapshot instead\"),\n            ),\n            Self::Done => Ok(EitherFunctionSnapshot::Done),\n        }\n    }\n\n    /// Converts into `EitherFunctionSnapshot` with a REPL owner attached.\n    ///\n    /// REPL variants are wired to the given `Py<PyMontyRepl>`.\n    /// Non-REPL variants pass through unchanged.\n    fn into_either_with_repl(self, owner: Py<PyMontyRepl>) -> EitherFunctionSnapshot {\n        match self {\n            Self::NoLimitFn(c) => EitherFunctionSnapshot::NoLimitFn(c),\n            Self::NoLimitOs(c) => EitherFunctionSnapshot::NoLimitOs(c),\n            Self::LimitedFn(c) => EitherFunctionSnapshot::LimitedFn(c),\n            Self::LimitedOs(c) => EitherFunctionSnapshot::LimitedOs(c),\n            Self::ReplNoLimitFn(c) => EitherFunctionSnapshot::ReplNoLimitFn(c, owner),\n            Self::ReplNoLimitOs(c) => EitherFunctionSnapshot::ReplNoLimitOs(c, owner),\n            Self::ReplLimitedFn(c) => EitherFunctionSnapshot::ReplLimitedFn(c, owner),\n            Self::ReplLimitedOs(c) => EitherFunctionSnapshot::ReplLimitedOs(c, owner),\n            Self::Done => EitherFunctionSnapshot::Done,\n        }\n    }\n}\n\nimpl EitherFunctionSnapshot {\n    /// Borrows self as a `SerdeFunctionSnapshotRef` for serialization.\n    fn as_serde_ref(&self) -> SerdeFunctionSnapshotRef<'_> {\n        match self {\n            Self::NoLimitFn(c) => SerdeFunctionSnapshotRef::NoLimitFn(c),\n            Self::NoLimitOs(c) => SerdeFunctionSnapshotRef::NoLimitOs(c),\n            Self::LimitedFn(c) => SerdeFunctionSnapshotRef::LimitedFn(c),\n            Self::LimitedOs(c) => SerdeFunctionSnapshotRef::LimitedOs(c),\n            Self::ReplNoLimitFn(c, _) => SerdeFunctionSnapshotRef::ReplNoLimitFn(c),\n            Self::ReplNoLimitOs(c, _) => SerdeFunctionSnapshotRef::ReplNoLimitOs(c),\n            Self::ReplLimitedFn(c, _) => SerdeFunctionSnapshotRef::ReplLimitedFn(c),\n            Self::ReplLimitedOs(c, _) => SerdeFunctionSnapshotRef::ReplLimitedOs(c),\n            Self::Done => SerdeFunctionSnapshotRef::Done,\n        }\n    }\n}\n\n/// Wire-format representation of `EitherLookupSnapshot` without `Py<PyMontyRepl>`.\n#[derive(Serialize, Deserialize)]\npub(crate) enum SerdeLookupSnapshot {\n    NoLimit(NameLookup<PySignalTracker<NoLimitTracker>>),\n    Limited(NameLookup<PySignalTracker<LimitedTracker>>),\n    ReplNoLimit(ReplNameLookup<PySignalTracker<NoLimitTracker>>),\n    ReplLimited(ReplNameLookup<PySignalTracker<LimitedTracker>>),\n    Done,\n}\n\n/// Borrowing version of `SerdeLookupSnapshot` for zero-copy serialization.\n#[derive(Serialize)]\nenum SerdeLookupSnapshotRef<'a> {\n    NoLimit(&'a NameLookup<PySignalTracker<NoLimitTracker>>),\n    Limited(&'a NameLookup<PySignalTracker<LimitedTracker>>),\n    ReplNoLimit(&'a ReplNameLookup<PySignalTracker<NoLimitTracker>>),\n    ReplLimited(&'a ReplNameLookup<PySignalTracker<LimitedTracker>>),\n    Done,\n}\n\nimpl SerdeLookupSnapshot {\n    /// Converts into `EitherLookupSnapshot` for the non-REPL path.\n    fn into_either(self) -> PyResult<EitherLookupSnapshot> {\n        match self {\n            Self::NoLimit(l) => Ok(EitherLookupSnapshot::NoLimit(l)),\n            Self::Limited(l) => Ok(EitherLookupSnapshot::Limited(l)),\n            Self::ReplNoLimit(_) | Self::ReplLimited(_) => Err(PyValueError::new_err(\n                \"Cannot load a REPL snapshot with load_snapshot, use load_repl_snapshot instead\",\n            )),\n            Self::Done => Ok(EitherLookupSnapshot::Done),\n        }\n    }\n\n    /// Converts into `EitherLookupSnapshot` with a REPL owner attached.\n    fn into_either_with_repl(self, owner: Py<PyMontyRepl>) -> EitherLookupSnapshot {\n        match self {\n            Self::NoLimit(l) => EitherLookupSnapshot::NoLimit(l),\n            Self::Limited(l) => EitherLookupSnapshot::Limited(l),\n            Self::ReplNoLimit(l) => EitherLookupSnapshot::ReplNoLimit(l, owner),\n            Self::ReplLimited(l) => EitherLookupSnapshot::ReplLimited(l, owner),\n            Self::Done => EitherLookupSnapshot::Done,\n        }\n    }\n}\n\nimpl EitherLookupSnapshot {\n    /// Borrows self as a `SerdeLookupSnapshotRef` for serialization.\n    fn as_serde_ref(&self) -> SerdeLookupSnapshotRef<'_> {\n        match self {\n            Self::NoLimit(l) => SerdeLookupSnapshotRef::NoLimit(l),\n            Self::Limited(l) => SerdeLookupSnapshotRef::Limited(l),\n            Self::ReplNoLimit(l, _) => SerdeLookupSnapshotRef::ReplNoLimit(l),\n            Self::ReplLimited(l, _) => SerdeLookupSnapshotRef::ReplLimited(l),\n            Self::Done => SerdeLookupSnapshotRef::Done,\n        }\n    }\n}\n\n/// Wire-format representation of `EitherFutureSnapshot` without `Py<PyMontyRepl>`.\n#[derive(Serialize, Deserialize)]\npub(crate) enum SerdeFutureSnapshot {\n    NoLimit(ResolveFutures<PySignalTracker<NoLimitTracker>>),\n    Limited(ResolveFutures<PySignalTracker<LimitedTracker>>),\n    ReplNoLimit(ReplResolveFutures<PySignalTracker<NoLimitTracker>>),\n    ReplLimited(ReplResolveFutures<PySignalTracker<LimitedTracker>>),\n    Done,\n}\n\n/// Borrowing version of `SerdeFutureSnapshot` for zero-copy serialization.\n#[derive(Serialize)]\nenum SerdeFutureSnapshotRef<'a> {\n    NoLimit(&'a ResolveFutures<PySignalTracker<NoLimitTracker>>),\n    Limited(&'a ResolveFutures<PySignalTracker<LimitedTracker>>),\n    ReplNoLimit(&'a ReplResolveFutures<PySignalTracker<NoLimitTracker>>),\n    ReplLimited(&'a ReplResolveFutures<PySignalTracker<LimitedTracker>>),\n    Done,\n}\n\nimpl SerdeFutureSnapshot {\n    /// Converts into `EitherFutureSnapshot` for the non-REPL path.\n    fn into_either(self) -> PyResult<EitherFutureSnapshot> {\n        match self {\n            Self::NoLimit(s) => Ok(EitherFutureSnapshot::NoLimit(s)),\n            Self::Limited(s) => Ok(EitherFutureSnapshot::Limited(s)),\n            Self::ReplNoLimit(_) | Self::ReplLimited(_) => Err(PyValueError::new_err(\n                \"Cannot load a REPL snapshot with load_snapshot, use load_repl_snapshot instead\",\n            )),\n            Self::Done => Ok(EitherFutureSnapshot::Done),\n        }\n    }\n\n    /// Converts into `EitherFutureSnapshot` with a REPL owner attached.\n    fn into_either_with_repl(self, owner: Py<PyMontyRepl>) -> EitherFutureSnapshot {\n        match self {\n            Self::NoLimit(s) => EitherFutureSnapshot::NoLimit(s),\n            Self::Limited(s) => EitherFutureSnapshot::Limited(s),\n            Self::ReplNoLimit(s) => EitherFutureSnapshot::ReplNoLimit(s, owner),\n            Self::ReplLimited(s) => EitherFutureSnapshot::ReplLimited(s, owner),\n            Self::Done => EitherFutureSnapshot::Done,\n        }\n    }\n}\n\nimpl EitherFutureSnapshot {\n    /// Borrows self as a `SerdeFutureSnapshotRef` for serialization.\n    fn as_serde_ref(&self) -> SerdeFutureSnapshotRef<'_> {\n        match self {\n            Self::NoLimit(s) => SerdeFutureSnapshotRef::NoLimit(s),\n            Self::Limited(s) => SerdeFutureSnapshotRef::Limited(s),\n            Self::ReplNoLimit(s, _) => SerdeFutureSnapshotRef::ReplNoLimit(s),\n            Self::ReplLimited(s, _) => SerdeFutureSnapshotRef::ReplLimited(s),\n            Self::Done => SerdeFutureSnapshotRef::Done,\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// dump helpers (called from #[pymethods] on each snapshot type)\n// ---------------------------------------------------------------------------\n\n/// Checks that a function snapshot hasn't been consumed, then serializes it.\n///\n/// For REPL variants, extracts the REPL state and produces `SerializedReplSnapshot`.\n/// For non-REPL variants, produces `SerializedSnapshot`.\n#[expect(clippy::too_many_arguments)]\npub(crate) fn dump_function_snapshot(\n    py: Python<'_>,\n    snapshot_mutex: &Mutex<EitherFunctionSnapshot>,\n    script_name: &str,\n    is_os_function: bool,\n    is_method_call: bool,\n    function_name: &str,\n    args: &Py<PyTuple>,\n    kwargs: &Py<PyDict>,\n    call_id: u32,\n    dc_registry: &DcRegistry,\n) -> PyResult<Vec<u8>> {\n    let snapshot = snapshot_mutex.lock().unwrap_or_else(PoisonError::into_inner);\n    if matches!(&*snapshot, EitherFunctionSnapshot::Done) {\n        return Err(PyRuntimeError::new_err(\n            \"Cannot dump progress that has already been resumed\",\n        ));\n    }\n\n    let args_monty = convert_args_to_monty(py, args, dc_registry)?;\n    let kwargs_monty = convert_kwargs_to_monty(py, kwargs, dc_registry)?;\n\n    let serde_ref = snapshot.as_serde_ref();\n\n    if snapshot.is_repl() {\n        let serialized = SerializedReplSnapshotRef::Function {\n            snapshot: serde_ref,\n            script_name,\n            is_os_function,\n            is_method_call,\n            function_name,\n            args: &args_monty,\n            kwargs: &kwargs_monty,\n            call_id,\n        };\n        serialize_with_header(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))\n    } else {\n        let serialized = SerializedSnapshotRef::Function {\n            snapshot: serde_ref,\n            script_name,\n            is_os_function,\n            is_method_call,\n            function_name,\n            args: &args_monty,\n            kwargs: &kwargs_monty,\n            call_id,\n        };\n        serialize_with_header(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))\n    }\n}\n\n/// Checks that a lookup snapshot hasn't been consumed, then serializes it.\npub(crate) fn dump_lookup_snapshot(\n    snapshot_mutex: &Mutex<EitherLookupSnapshot>,\n    script_name: &str,\n    variable_name: &str,\n) -> PyResult<Vec<u8>> {\n    let snapshot = snapshot_mutex.lock().unwrap_or_else(PoisonError::into_inner);\n    if matches!(&*snapshot, EitherLookupSnapshot::Done) {\n        return Err(PyRuntimeError::new_err(\n            \"Cannot dump progress that has already been resumed\",\n        ));\n    }\n\n    let serde_ref = snapshot.as_serde_ref();\n\n    if snapshot.is_repl() {\n        let serialized = SerializedReplSnapshotRef::NameLookup {\n            snapshot: serde_ref,\n            script_name,\n            variable_name,\n        };\n        serialize_with_header(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))\n    } else {\n        let serialized = SerializedSnapshotRef::NameLookup {\n            snapshot: serde_ref,\n            script_name,\n            variable_name,\n        };\n        serialize_with_header(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))\n    }\n}\n\n/// Checks that a future snapshot hasn't been consumed, then serializes it.\npub(crate) fn dump_future_snapshot(\n    snapshot_mutex: &Mutex<EitherFutureSnapshot>,\n    script_name: &str,\n) -> PyResult<Vec<u8>> {\n    let snapshot = snapshot_mutex.lock().unwrap_or_else(PoisonError::into_inner);\n    if matches!(&*snapshot, EitherFutureSnapshot::Done) {\n        return Err(PyRuntimeError::new_err(\n            \"Cannot dump progress that has already been resumed\",\n        ));\n    }\n\n    let serde_ref = snapshot.as_serde_ref();\n\n    if snapshot.is_repl() {\n        let serialized = SerializedReplSnapshotRef::Future {\n            snapshot: serde_ref,\n            script_name,\n        };\n        serialize_with_header(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))\n    } else {\n        let serialized = SerializedSnapshotRef::Future {\n            snapshot: serde_ref,\n            script_name,\n        };\n        serialize_with_header(&serialized).map_err(|e| PyValueError::new_err(e.to_string()))\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Borrowing serialization refs (avoid cloning large snapshot data)\n// ---------------------------------------------------------------------------\n\n/// Borrowing version of `SerializedSnapshot` for zero-copy serialization.\n#[derive(Serialize)]\nenum SerializedSnapshotRef<'a> {\n    Function {\n        snapshot: SerdeFunctionSnapshotRef<'a>,\n        script_name: &'a str,\n        is_os_function: bool,\n        is_method_call: bool,\n        function_name: &'a str,\n        args: &'a [MontyObject],\n        kwargs: &'a [(MontyObject, MontyObject)],\n        call_id: u32,\n    },\n    NameLookup {\n        snapshot: SerdeLookupSnapshotRef<'a>,\n        script_name: &'a str,\n        variable_name: &'a str,\n    },\n    Future {\n        snapshot: SerdeFutureSnapshotRef<'a>,\n        script_name: &'a str,\n    },\n}\n\n/// Borrowing version of `SerializedReplSnapshot` for zero-copy serialization.\n#[derive(Serialize)]\nenum SerializedReplSnapshotRef<'a> {\n    Function {\n        snapshot: SerdeFunctionSnapshotRef<'a>,\n        script_name: &'a str,\n        is_os_function: bool,\n        is_method_call: bool,\n        function_name: &'a str,\n        args: &'a [MontyObject],\n        kwargs: &'a [(MontyObject, MontyObject)],\n        call_id: u32,\n    },\n    NameLookup {\n        snapshot: SerdeLookupSnapshotRef<'a>,\n        script_name: &'a str,\n        variable_name: &'a str,\n    },\n    Future {\n        snapshot: SerdeFutureSnapshotRef<'a>,\n        script_name: &'a str,\n    },\n}\n\n// ---------------------------------------------------------------------------\n// Module-level load functions\n// ---------------------------------------------------------------------------\n\n/// Loads a non-REPL snapshot from bytes.\n///\n/// Returns `FunctionSnapshot | NameLookupSnapshot | FutureSnapshot` depending\n/// on what was serialized. Callers no longer need to know the snapshot type upfront.\n#[pyfunction]\n#[pyo3(signature = (data, *, print_callback=None, dataclass_registry=None))]\npub(crate) fn load_snapshot<'py>(\n    py: Python<'py>,\n    data: &Bound<'_, PyBytes>,\n    print_callback: Option<Py<PyAny>>,\n    dataclass_registry: Option<&Bound<'_, PyList>>,\n) -> PyResult<Bound<'py, PyAny>> {\n    let bytes = data.as_bytes();\n    let serialized: SerializedSnapshot = deserialize_with_header(bytes)?;\n    let dc_registry = DcRegistry::from_list(py, dataclass_registry)?;\n\n    match serialized {\n        SerializedSnapshot::Function {\n            snapshot,\n            script_name,\n            is_os_function,\n            is_method_call,\n            function_name,\n            args,\n            kwargs,\n            call_id,\n        } => {\n            let either = snapshot.into_either()?;\n            let py_args = monty_objects_to_py_tuple(py, &args, &dc_registry)?;\n            let py_kwargs = monty_pairs_to_py_dict(py, &kwargs, &dc_registry)?;\n            PyFunctionSnapshot::from_deserialized(\n                py,\n                either,\n                print_callback,\n                dc_registry,\n                script_name,\n                is_os_function,\n                is_method_call,\n                function_name,\n                py_args,\n                py_kwargs,\n                call_id,\n            )\n        }\n        SerializedSnapshot::NameLookup {\n            snapshot,\n            script_name,\n            variable_name,\n        } => {\n            let either = snapshot.into_either()?;\n            PyNameLookupSnapshot::from_deserialized(py, either, print_callback, dc_registry, script_name, variable_name)\n        }\n        SerializedSnapshot::Future { snapshot, script_name } => {\n            let either = snapshot.into_either()?;\n            PyFutureSnapshot::from_deserialized(py, either, print_callback, dc_registry, script_name)\n        }\n    }\n}\n\n/// Loads a REPL snapshot from bytes, returning `(snapshot, MontyRepl)`.\n///\n/// The REPL state is reconstructed into a fresh `PyMontyRepl` and the snapshot's\n/// REPL variant is rewired to point to it.\n#[pyfunction]\n#[pyo3(signature = (data, *, print_callback=None, dataclass_registry=None))]\npub(crate) fn load_repl_snapshot<'py>(\n    py: Python<'py>,\n    data: &Bound<'_, PyBytes>,\n    print_callback: Option<Py<PyAny>>,\n    dataclass_registry: Option<&Bound<'_, PyList>>,\n) -> PyResult<(Bound<'py, PyAny>, Py<PyMontyRepl>)> {\n    let bytes = data.as_bytes();\n    let serialized: SerializedReplSnapshot = deserialize_with_header(bytes)?;\n    let dc_registry = DcRegistry::from_list(py, dataclass_registry)?;\n\n    match serialized {\n        SerializedReplSnapshot::Function {\n            snapshot,\n            script_name,\n            is_os_function,\n            is_method_call,\n            function_name,\n            args,\n            kwargs,\n            call_id,\n        } => {\n            let repl_py = create_empty_py_repl(py, &script_name, &dc_registry)?;\n            let either = snapshot.into_either_with_repl(repl_py.clone_ref(py));\n            let py_args = monty_objects_to_py_tuple(py, &args, &dc_registry)?;\n            let py_kwargs = monty_pairs_to_py_dict(py, &kwargs, &dc_registry)?;\n            let snap = PyFunctionSnapshot::from_deserialized(\n                py,\n                either,\n                print_callback,\n                dc_registry,\n                script_name,\n                is_os_function,\n                is_method_call,\n                function_name,\n                py_args,\n                py_kwargs,\n                call_id,\n            )?;\n            Ok((snap, repl_py))\n        }\n        SerializedReplSnapshot::NameLookup {\n            snapshot,\n            script_name,\n            variable_name,\n        } => {\n            let repl_py = create_empty_py_repl(py, &script_name, &dc_registry)?;\n            let either = snapshot.into_either_with_repl(repl_py.clone_ref(py));\n            let snap = PyNameLookupSnapshot::from_deserialized(\n                py,\n                either,\n                print_callback,\n                dc_registry,\n                script_name,\n                variable_name,\n            )?;\n            Ok((snap, repl_py))\n        }\n        SerializedReplSnapshot::Future { snapshot, script_name } => {\n            let repl_py = create_empty_py_repl(py, &script_name, &dc_registry)?;\n            let either = snapshot.into_either_with_repl(repl_py.clone_ref(py));\n            let snap = PyFutureSnapshot::from_deserialized(py, either, print_callback, dc_registry, script_name)?;\n            Ok((snap, repl_py))\n        }\n    }\n}\n\n/// Creates an empty `Py<PyMontyRepl>` for use as a REPL owner reference.\n///\n/// The REPL starts with `None` inside — the real REPL state lives inside the\n/// snapshot and will be restored via `put_repl` when the snapshot completes.\nfn create_empty_py_repl(py: Python<'_>, script_name: &str, dc_registry: &DcRegistry) -> PyResult<Py<PyMontyRepl>> {\n    let repl_obj = PyMontyRepl::empty_owner(script_name.to_owned(), dc_registry.clone_ref(py));\n    Py::new(py, repl_obj)\n}\n\n// ---------------------------------------------------------------------------\n// Conversion helpers\n// ---------------------------------------------------------------------------\n\n/// Converts a `Py<PyTuple>` of Python args to `Vec<MontyObject>`.\nfn convert_args_to_monty(py: Python<'_>, args: &Py<PyTuple>, dc_registry: &DcRegistry) -> PyResult<Vec<MontyObject>> {\n    args.bind(py)\n        .iter()\n        .map(|item| py_to_monty(&item, dc_registry))\n        .collect()\n}\n\n/// Converts a `Py<PyDict>` of Python kwargs to `Vec<(MontyObject, MontyObject)>`.\nfn convert_kwargs_to_monty(\n    py: Python<'_>,\n    kwargs: &Py<PyDict>,\n    dc_registry: &DcRegistry,\n) -> PyResult<Vec<(MontyObject, MontyObject)>> {\n    kwargs\n        .bind(py)\n        .iter()\n        .map(|(k, v)| Ok((py_to_monty(&k, dc_registry)?, py_to_monty(&v, dc_registry)?)))\n        .collect()\n}\n\n/// Converts `&[MontyObject]` to a Python tuple.\nfn monty_objects_to_py_tuple(\n    py: Python<'_>,\n    objects: &[MontyObject],\n    dc_registry: &DcRegistry,\n) -> PyResult<Py<PyTuple>> {\n    let items: Vec<Py<PyAny>> = objects\n        .iter()\n        .map(|item| monty_to_py(py, item, dc_registry))\n        .collect::<PyResult<_>>()?;\n    Ok(PyTuple::new(py, items)?.unbind())\n}\n\n/// Converts `&[(MontyObject, MontyObject)]` to a Python dict.\nfn monty_pairs_to_py_dict(\n    py: Python<'_>,\n    pairs: &[(MontyObject, MontyObject)],\n    dc_registry: &DcRegistry,\n) -> PyResult<Py<PyDict>> {\n    let dict = PyDict::new(py);\n    for (k, v) in pairs {\n        dict.set_item(monty_to_py(py, k, dc_registry)?, monty_to_py(py, v, dc_registry)?)?;\n    }\n    Ok(dict.unbind())\n}\n\n// ---------------------------------------------------------------------------\n// Trait extensions on Either*Snapshot for REPL detection and state extraction\n// ---------------------------------------------------------------------------\n\nimpl EitherFunctionSnapshot {\n    /// Returns `true` if this snapshot is from a REPL `feed_start()` call.\n    pub(crate) fn is_repl(&self) -> bool {\n        matches!(\n            self,\n            Self::ReplNoLimitFn(..) | Self::ReplNoLimitOs(..) | Self::ReplLimitedFn(..) | Self::ReplLimitedOs(..)\n        )\n    }\n}\n\nimpl EitherLookupSnapshot {\n    /// Returns `true` if this snapshot is from a REPL `feed_start()` call.\n    pub(crate) fn is_repl(&self) -> bool {\n        matches!(self, Self::ReplNoLimit(..) | Self::ReplLimited(..))\n    }\n}\n\nimpl EitherFutureSnapshot {\n    /// Returns `true` if this snapshot is from a REPL `feed_start()` call.\n    pub(crate) fn is_repl(&self) -> bool {\n        matches!(self, Self::ReplNoLimit(..) | Self::ReplLimited(..))\n    }\n}\n"
  },
  {
    "path": "crates/monty-python/tests/test_async.py",
    "content": "import asyncio\n\nimport pytest\nfrom dirty_equals import IsList\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\nfrom pydantic_monty import run_monty_async, run_repl_async\n\n\ndef test_async():\n    code = 'await foobar(1, 2)'\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('foobar')\n    assert progress.args == snapshot((1, 2))\n    call_id = progress.call_id\n    progress = progress.resume(future=...)\n    assert isinstance(progress, pydantic_monty.FutureSnapshot)\n    assert progress.pending_call_ids == snapshot([call_id])\n    progress = progress.resume({call_id: {'return_value': 3}})\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(3)\n\n\ndef test_asyncio_gather():\n    code = \"\"\"\nimport asyncio\n\nawait asyncio.gather(foo(1), bar(2))\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('foo')\n    assert progress.args == snapshot((1,))\n    foo_call_ids = progress.call_id\n\n    progress = progress.resume(future=...)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('bar')\n    assert progress.args == snapshot((2,))\n    bar_call_ids = progress.call_id\n    progress = progress.resume(future=...)\n\n    assert isinstance(progress, pydantic_monty.FutureSnapshot)\n    dump_progress = progress.dump()\n\n    assert progress.pending_call_ids == IsList(foo_call_ids, bar_call_ids, check_order=False)\n    progress = progress.resume({foo_call_ids: {'return_value': 3}, bar_call_ids: {'return_value': 4}})\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot([3, 4])\n\n    progress2 = pydantic_monty.load_snapshot(dump_progress)\n    assert isinstance(progress2, pydantic_monty.FutureSnapshot)\n    assert progress2.pending_call_ids == IsList(foo_call_ids, bar_call_ids, check_order=False)\n    progress = progress2.resume({bar_call_ids: {'return_value': 14}, foo_call_ids: {'return_value': 13}})\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot([13, 14])\n\n    progress3 = pydantic_monty.load_snapshot(dump_progress)\n    assert isinstance(progress3, pydantic_monty.FutureSnapshot)\n    progress = progress3.resume({bar_call_ids: {'return_value': 14}, foo_call_ids: {'future': ...}})\n    assert isinstance(progress, pydantic_monty.FutureSnapshot)\n\n    assert progress.pending_call_ids == [foo_call_ids]\n    progress = progress.resume({foo_call_ids: {'return_value': 144}})\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot([144, 14])\n\n\n# === Tests for run_monty_async ===\n\n\nasync def test_run_monty_async_sync_function():\n    \"\"\"Test run_monty_async with a basic sync external function.\"\"\"\n    m = pydantic_monty.Monty('get_value()')\n\n    def get_value():\n        return 42\n\n    result = await run_monty_async(m, external_functions={'get_value': get_value})\n    assert result == snapshot(42)\n\n\nasync def test_run_monty_async_async_function():\n    \"\"\"Test run_monty_async with a basic async external function.\"\"\"\n    m = pydantic_monty.Monty('await fetch_data()')\n\n    async def fetch_data():\n        await asyncio.sleep(0.001)\n        return 'async result'\n\n    result = await run_monty_async(m, external_functions={'fetch_data': fetch_data})\n    assert result == snapshot('async result')\n\n\nasync def test_run_monty_async_function_not_found():\n    \"\"\"Test that missing external function raises wrapped error.\"\"\"\n    m = pydantic_monty.Monty('missing_func()')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        await run_monty_async(m, external_functions={})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, LookupError)\n    assert inner.args[0] == snapshot(\"Unable to find 'missing_func' in external functions dict\")\n\n\nasync def test_run_monty_async_sync_exception():\n    \"\"\"Test that sync function exceptions propagate correctly.\"\"\"\n    m = pydantic_monty.Monty('fail()')\n\n    def fail():\n        raise ValueError('sync error')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        await run_monty_async(m, external_functions={'fail': fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('sync error')\n\n\nasync def test_run_monty_async_async_exception():\n    \"\"\"Test that async function exceptions propagate correctly.\"\"\"\n    m = pydantic_monty.Monty('await async_fail()')\n\n    async def async_fail():\n        await asyncio.sleep(0.001)\n        raise RuntimeError('async error')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        await run_monty_async(m, external_functions={'async_fail': async_fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, RuntimeError)\n    assert inner.args[0] == snapshot('async error')\n\n\nasync def test_run_monty_async_exception_caught():\n    \"\"\"Test that exceptions caught in try/except don't propagate.\"\"\"\n    code = \"\"\"\ntry:\n    fail()\nexcept ValueError:\n    caught = True\ncaught\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def fail():\n        raise ValueError('caught error')\n\n    result = await run_monty_async(m, external_functions={'fail': fail})\n    assert result == snapshot(True)\n\n\nasync def test_run_monty_async_multiple_async_functions():\n    \"\"\"Test asyncio.gather with multiple async functions.\"\"\"\n    code = \"\"\"\nimport asyncio\nawait asyncio.gather(fetch_a(), fetch_b())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    async def fetch_a():\n        await asyncio.sleep(0.01)\n        return 'a'\n\n    async def fetch_b():\n        await asyncio.sleep(0.005)\n        return 'b'\n\n    result = await run_monty_async(m, external_functions={'fetch_a': fetch_a, 'fetch_b': fetch_b})\n    assert result == snapshot(['a', 'b'])\n\n\nasync def test_run_monty_async_mixed_sync_async():\n    \"\"\"Test mix of sync and async external functions.\"\"\"\n    code = \"\"\"\nsync_val = sync_func()\nasync_val = await async_func()\nsync_val + async_val\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def sync_func():\n        return 10\n\n    async def async_func():\n        await asyncio.sleep(0.001)\n        return 5\n\n    result = await run_monty_async(m, external_functions={'sync_func': sync_func, 'async_func': async_func})\n    assert result == snapshot(15)\n\n\nasync def test_run_monty_async_with_inputs():\n    \"\"\"Test run_monty_async with inputs parameter.\"\"\"\n    m = pydantic_monty.Monty('process(x, y)', inputs=['x', 'y'])\n\n    def process(a: int, b: int) -> int:\n        return a * b\n\n    result = await run_monty_async(m, inputs={'x': 6, 'y': 7}, external_functions={'process': process})\n    assert result == snapshot(42)\n\n\nasync def test_run_monty_async_with_print_callback():\n    \"\"\"Test run_monty_async with print_callback parameter.\"\"\"\n    output: list[tuple[str, str]] = []\n\n    def callback(stream: str, text: str) -> None:\n        output.append((stream, text))\n\n    m = pydantic_monty.Monty('print(\"hello from async\")')\n    result = await run_monty_async(m, print_callback=callback)\n    assert result is None\n    assert output == snapshot([('stdout', 'hello from async'), ('stdout', '\\n')])\n\n\nasync def test_run_monty_async_function_returning_none():\n    \"\"\"Test async function that returns None.\"\"\"\n    m = pydantic_monty.Monty('do_nothing()')\n\n    def do_nothing():\n        return None\n\n    result = await run_monty_async(m, external_functions={'do_nothing': do_nothing})\n    assert result is None\n\n\nasync def test_run_monty_async_no_external_calls():\n    \"\"\"Test run_monty_async when code has no external calls.\"\"\"\n    m = pydantic_monty.Monty('1 + 2 + 3')\n    result = await run_monty_async(m)\n    assert result == snapshot(6)\n\n\n# === Tests for run_monty_async with os parameter ===\n\n\nasync def test_run_monty_async_with_os():\n    \"\"\"run_monty_async can use OSAccess for file operations.\"\"\"\n    from pydantic_monty import MemoryFile, OSAccess\n\n    fs = OSAccess([MemoryFile('/test.txt', content='hello world')])\n\n    m = pydantic_monty.Monty(\n        \"\"\"\nfrom pathlib import Path\nPath('/test.txt').read_text()\n        \"\"\",\n    )\n\n    result = await run_monty_async(m, os=fs)\n    assert result == snapshot('hello world')\n\n\nasync def test_run_monty_async_os_with_external_functions():\n    \"\"\"run_monty_async can combine OSAccess with external functions.\"\"\"\n    from pydantic_monty import MemoryFile, OSAccess\n\n    fs = OSAccess([MemoryFile('/data.txt', content='test data')])\n\n    async def process(text: str) -> str:\n        return text.upper()\n\n    m = pydantic_monty.Monty(\n        \"\"\"\nfrom pathlib import Path\ncontent = Path('/data.txt').read_text()\nawait process(content)\n        \"\"\",\n    )\n\n    result = await run_monty_async(\n        m,\n        external_functions={'process': process},\n        os=fs,\n    )\n    assert result == snapshot('TEST DATA')\n\n\nasync def test_run_monty_async_os_file_not_found():\n    \"\"\"run_monty_async propagates OS errors correctly.\"\"\"\n    from pydantic_monty import OSAccess\n\n    fs = OSAccess()\n\n    m = pydantic_monty.Monty(\n        \"\"\"\nfrom pathlib import Path\nPath('/missing.txt').read_text()\n        \"\"\",\n    )\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        await run_monty_async(m, os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing.txt'\")\n\n\nasync def test_run_monty_async_os_not_provided():\n    \"\"\"run_monty_async raises error when OS function called without os handler.\"\"\"\n    m = pydantic_monty.Monty(\n        \"\"\"\nfrom pathlib import Path\nPath('/test.txt').exists()\n        \"\"\",\n    )\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        await run_monty_async(m)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, RuntimeError)\n    assert 'OS function' in inner.args[0]\n    assert 'no os handler provided' in inner.args[0]\n\n\nasync def test_run_monty_async_nested_gather_with_external_functions():\n    \"\"\"Test nested asyncio.gather with spawned tasks and external async functions.\n\n    https://github.com/pydantic/monty/pull/174\n\n    Reproduces the pattern from stack_overflow.py: outer gather spawns 3 coroutine tasks,\n    each doing a sequential await then an inner gather with 2 external futures.\n    \"\"\"\n    code = \"\"\"\\\nimport asyncio\n\nasync def get_city_weather(city_name: str):\n    coords = await get_lat_lng(location_description=city_name)\n    lat, lng = coords['lat'], coords['lng']\n    temp_task = get_temp(lat=lat, lng=lng)\n    desc_task = get_weather_description(lat=lat, lng=lng)\n    temp, desc = await asyncio.gather(temp_task, desc_task)\n    return {\n        'city': city_name,\n        'temp': temp,\n        'description': desc\n    }\n\nasync def main():\n    cities = ['London', 'Paris', 'Tokyo']\n    results = await asyncio.gather(*(get_city_weather(city) for city in cities))\n    return results\n\nawait main()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    city_coords = {\n        'London': {'lat': 51.5, 'lng': -0.1},\n        'Paris': {'lat': 48.9, 'lng': 2.3},\n        'Tokyo': {'lat': 35.7, 'lng': 139.7},\n    }\n    city_temps = {\n        (51.5, -0.1): 15.0,\n        (48.9, 2.3): 18.0,\n        (35.7, 139.7): 22.0,\n    }\n    city_descs = {\n        (51.5, -0.1): 'Cloudy',\n        (48.9, 2.3): 'Sunny',\n        (35.7, 139.7): 'Humid',\n    }\n\n    async def get_lat_lng(location_description: str):\n        return city_coords[location_description]\n\n    async def get_temp(lat: float, lng: float):\n        return city_temps[(lat, lng)]\n\n    async def get_weather_description(lat: float, lng: float):\n        return city_descs[(lat, lng)]\n\n    result = await run_monty_async(\n        m,\n        external_functions={\n            'get_lat_lng': get_lat_lng,\n            'get_temp': get_temp,\n            'get_weather_description': get_weather_description,\n        },\n    )\n    assert result == snapshot(\n        [\n            {'city': 'London', 'temp': 15.0, 'description': 'Cloudy'},\n            {'city': 'Paris', 'temp': 18.0, 'description': 'Sunny'},\n            {'city': 'Tokyo', 'temp': 22.0, 'description': 'Humid'},\n        ]\n    )\n\n\nasync def test_run_monty_async_os_write_and_read():\n    \"\"\"run_monty_async supports both reading and writing files.\"\"\"\n    from pydantic_monty import MemoryFile, OSAccess\n\n    fs = OSAccess([MemoryFile('/file.txt', content='original')])\n\n    m = pydantic_monty.Monty(\n        \"\"\"\nfrom pathlib import Path\np = Path('/file.txt')\np.write_text('updated')\np.read_text()\n        \"\"\",\n    )\n\n    result = await run_monty_async(m, os=fs)\n    assert result == snapshot('updated')\n\n\n# === Tests for MontyRepl.feed_start() with async patterns ===\n\n\ndef test_repl_feed_start_async_gather():\n    \"\"\"MontyRepl.feed_start supports asyncio.gather with multiple futures.\"\"\"\n    code = \"\"\"\nimport asyncio\n\nawait asyncio.gather(foo(1), bar(2))\n\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start(code)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('foo')\n    foo_call_id = progress.call_id\n\n    progress = progress.resume(future=...)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('bar')\n    bar_call_id = progress.call_id\n    progress = progress.resume(future=...)\n\n    assert isinstance(progress, pydantic_monty.FutureSnapshot)\n    from dirty_equals import IsList\n\n    assert progress.pending_call_ids == IsList(foo_call_id, bar_call_id, check_order=False)\n    progress = progress.resume({foo_call_id: {'return_value': 3}, bar_call_id: {'return_value': 4}})\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot([3, 4])\n\n    # REPL should still be usable after async completion\n    assert repl.feed_run('1 + 1') == snapshot(2)\n\n\ndef test_repl_feed_start_async_state_persistence():\n    \"\"\"MontyRepl.feed_start async: REPL state persists across async snippets.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 10')\n\n    progress = repl.feed_start('result = await fetch(x)')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('fetch')\n    assert progress.args == snapshot((10,))\n    call_id = progress.call_id\n\n    progress = progress.resume(future=...)\n    assert isinstance(progress, pydantic_monty.FutureSnapshot)\n    progress = progress.resume({call_id: {'return_value': 'fetched'}})\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output is None  # assignment, not expression\n\n    assert repl.feed_run('result') == snapshot('fetched')\n    assert repl.feed_run('x') == snapshot(10)\n\n\n# === Tests for run_repl_async ===\n\n\nasync def test_run_repl_async_sync_function():\n    \"\"\"run_repl_async with a basic sync external function.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    def get_value():\n        return 42\n\n    result = await run_repl_async(repl, 'get_value()', external_functions={'get_value': get_value})\n    assert result == snapshot(42)\n\n\nasync def test_run_repl_async_async_function():\n    \"\"\"run_repl_async with a basic async external function.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def fetch_data():\n        await asyncio.sleep(0.001)\n        return 'async result'\n\n    result = await run_repl_async(repl, 'await fetch_data()', external_functions={'fetch_data': fetch_data})\n    assert result == snapshot('async result')\n\n\nasync def test_run_repl_async_state_persists():\n    \"\"\"REPL state persists across multiple run_repl_async calls.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    def double(x: int) -> int:\n        return x * 2\n\n    ext = {'double': double}\n    await run_repl_async(repl, 'x = 10', external_functions=ext)\n    await run_repl_async(repl, 'y = double(x)', external_functions=ext)\n    result = await run_repl_async(repl, 'y', external_functions=ext)\n    assert result == snapshot(20)\n\n\nasync def test_run_repl_async_async_state_persists():\n    \"\"\"REPL state persists across async calls with await.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def fetch(key: str) -> str:\n        return f'value_{key}'\n\n    ext = {'fetch': fetch}\n    await run_repl_async(repl, \"a = await fetch('one')\", external_functions=ext)\n    await run_repl_async(repl, \"b = await fetch('two')\", external_functions=ext)\n    result = await run_repl_async(repl, 'a + b', external_functions=ext)\n    assert result == snapshot('value_onevalue_two')\n\n\nasync def test_run_repl_async_gather():\n    \"\"\"run_repl_async handles asyncio.gather with multiple futures.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def fetch_a():\n        await asyncio.sleep(0.01)\n        return 'a'\n\n    async def fetch_b():\n        await asyncio.sleep(0.005)\n        return 'b'\n\n    code = \"\"\"\\\nimport asyncio\nawait asyncio.gather(fetch_a(), fetch_b())\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions={'fetch_a': fetch_a, 'fetch_b': fetch_b})\n    assert result == snapshot(['a', 'b'])\n\n\nasync def test_run_repl_async_function_not_found():\n    \"\"\"run_repl_async raises error for missing external function.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        await run_repl_async(repl, 'missing_func()', external_functions={})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, LookupError)\n    assert inner.args[0] == snapshot(\"Unable to find 'missing_func' in external functions dict\")\n\n\nasync def test_run_repl_async_error_preserves_state():\n    \"\"\"REPL state is preserved after an error in run_repl_async.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    await run_repl_async(repl, 'x = 42')\n\n    def fail():\n        raise ValueError('oops')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        await run_repl_async(repl, 'fail()', external_functions={'fail': fail})\n\n    result = await run_repl_async(repl, 'x')\n    assert result == snapshot(42)\n\n\nasync def test_run_repl_async_with_inputs():\n    \"\"\"run_repl_async supports inputs parameter.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    result = await run_repl_async(repl, 'add(x, y)', inputs={'x': 3, 'y': 4}, external_functions={'add': add})\n    assert result == snapshot(7)\n\n\nasync def test_run_repl_async_with_print_callback():\n    \"\"\"run_repl_async supports print_callback parameter.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    output: list[str] = []\n\n    def callback(stream: str, text: str) -> None:\n        output.append(text)\n\n    await run_repl_async(repl, 'print(\"hello from repl\")', print_callback=callback)\n    assert output == snapshot(['hello from repl', '\\n'])\n\n\nasync def test_run_repl_async_with_os():\n    \"\"\"run_repl_async supports OS access.\"\"\"\n    from pydantic_monty import MemoryFile, OSAccess\n\n    repl = pydantic_monty.MontyRepl()\n    fs = OSAccess([MemoryFile('/test.txt', content='repl content')])\n\n    code = \"\"\"\\\nfrom pathlib import Path\nPath('/test.txt').read_text()\n\"\"\"\n    result = await run_repl_async(repl, code, os=fs)\n    assert result == snapshot('repl content')\n\n\nasync def test_run_repl_async_mixed_sync_async():\n    \"\"\"run_repl_async handles mix of sync and async functions.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    def sync_func():\n        return 10\n\n    async def async_func():\n        await asyncio.sleep(0.001)\n        return 5\n\n    code = \"\"\"\\\nsync_val = sync_func()\nasync_val = await async_func()\nsync_val + async_val\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions={'sync_func': sync_func, 'async_func': async_func})\n    assert result == snapshot(15)\n\n\nasync def test_run_repl_async_no_external_calls():\n    \"\"\"run_repl_async works when code has no external calls.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    result = await run_repl_async(repl, '1 + 2 + 3')\n    assert result == snapshot(6)\n\n\n# === LLM agent patterns: realistic run_repl_async scenarios ===\n\n\nasync def test_repl_llm_iterative_data_collection():\n    \"\"\"LLM defines a helper, collects data in batches, accumulates results across snippets.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    responses: dict[int, list[dict[str, object]]] = {\n        0: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}],\n        2: [{'id': 3, 'name': 'Charlie'}],\n        3: [],\n    }\n\n    async def fetch_users(offset: int, limit: int) -> list[dict[str, object]]:\n        return responses.get(offset, [])\n\n    ext = {'fetch_users': fetch_users}\n\n    # Snippet 1: LLM sets up accumulator\n    await run_repl_async(repl, 'all_users = []', external_functions=ext)\n\n    # Snippet 2: LLM fetches first batch\n    await run_repl_async(\n        repl,\n        \"\"\"\\\nbatch = await fetch_users(0, 2)\nall_users = all_users + batch\nlen(batch)\n\"\"\",\n        external_functions=ext,\n    )\n\n    # Snippet 3: LLM fetches next batch using state\n    await run_repl_async(\n        repl,\n        \"\"\"\\\nbatch = await fetch_users(len(all_users), 2)\nall_users = all_users + batch\nlen(batch)\n\"\"\",\n        external_functions=ext,\n    )\n\n    # Snippet 4: LLM fetches again, gets empty — realizes done\n    await run_repl_async(\n        repl,\n        \"\"\"\\\nbatch = await fetch_users(len(all_users), 2)\nall_users = all_users + batch\nlen(batch)\n\"\"\",\n        external_functions=ext,\n    )\n\n    # Snippet 5: LLM extracts final result\n    result = await run_repl_async(repl, '[u[\"name\"] for u in all_users]', external_functions=ext)\n    assert result == snapshot(['Alice', 'Bob', 'Charlie'])\n\n\nasync def test_repl_llm_error_recovery_retry():\n    \"\"\"LLM catches an error, adjusts approach, retries successfully.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    call_count = 0\n\n    async def flaky_api(query: str) -> str:\n        nonlocal call_count\n        call_count += 1\n        if call_count == 1:\n            raise ConnectionError('server unavailable')\n        return f'result for {query}'\n\n    ext = {'flaky_api': flaky_api}\n\n    # Snippet 1: LLM tries, gets error\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        await run_repl_async(repl, \"data = await flaky_api('test')\", external_functions=ext)\n\n    # Snippet 2: LLM wraps in try/except and retries\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\ntry:\n    data = await flaky_api('test')\nexcept Exception as e:\n    data = 'fallback'\ndata\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot('result for test')\n\n\nasync def test_repl_llm_redefine_helper_function():\n    \"\"\"LLM defines a function, uses it, then redefines it with improvements.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def fetch(url: str) -> str:\n        return f'<html>{url}</html>'\n\n    ext = {'fetch': fetch}\n\n    # Snippet 1: LLM defines initial parser\n    await run_repl_async(\n        repl,\n        \"\"\"\\\ndef parse_title(html):\n    return html\n\"\"\",\n        external_functions=ext,\n    )\n\n    # Snippet 2: LLM uses it, gets raw html back\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\nhtml = await fetch('example.com')\nparse_title(html)\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot('<html>example.com</html>')\n\n    # Snippet 3: LLM redefines parser with better logic\n    await run_repl_async(\n        repl,\n        \"\"\"\\\ndef parse_title(html):\n    start = html.find('>') + 1\n    end = html.rfind('<')\n    return html[start:end]\n\"\"\",\n        external_functions=ext,\n    )\n\n    # Snippet 4: uses improved parser on previously fetched data\n    result = await run_repl_async(repl, 'parse_title(html)', external_functions=ext)\n    assert result == snapshot('example.com')\n\n\nasync def test_repl_llm_sequential_async_pipeline():\n    \"\"\"LLM builds a data pipeline: fetch -> transform -> store, each step depends on previous.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def search(query: str) -> list[str]:\n        return [f'{query}_result_1', f'{query}_result_2']\n\n    async def summarize(text: str) -> str:\n        return f'summary({text})'\n\n    records: list[str] = []\n\n    def record(item: str) -> None:\n        records.append(item)\n\n    ext = {'search': search, 'summarize': summarize, 'record': record}\n\n    code = \"\"\"\\\nresults = await search('python async')\nsummaries = []\nfor r in results:\n    s = await summarize(r)\n    summaries.append(s)\n    record(s)\nsummaries\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot(['summary(python async_result_1)', 'summary(python async_result_2)'])\n    assert records == snapshot(['summary(python async_result_1)', 'summary(python async_result_2)'])\n\n\nasync def test_repl_llm_gather_fan_out():\n    \"\"\"LLM uses asyncio.gather to fan out many concurrent requests.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def fetch_price(item: str) -> float:\n        prices = {'apple': 1.5, 'banana': 0.75, 'cherry': 3.0, 'date': 5.0, 'elderberry': 8.0}\n        return prices[item]\n\n    ext = {'fetch_price': fetch_price}\n\n    code = \"\"\"\\\nimport asyncio\n\nitems = ['apple', 'banana', 'cherry', 'date', 'elderberry']\nprices = await asyncio.gather(*(fetch_price(item) for item in items))\ndict(zip(items, prices))\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot({'apple': 1.5, 'banana': 0.75, 'cherry': 3.0, 'date': 5.0, 'elderberry': 8.0})\n\n\nasync def test_repl_llm_try_except_around_external():\n    \"\"\"LLM wraps individual external calls in try/except for graceful degradation.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    def fetch_data(key: str) -> str:\n        if key == 'bad':\n            raise KeyError(f'no data for {key}')\n        return f'data_{key}'\n\n    ext = {'fetch_data': fetch_data}\n\n    code = \"\"\"\\\nresults = {}\nfor key in ['good', 'bad', 'also_good']:\n    try:\n        results[key] = fetch_data(key)\n    except KeyError:\n        results[key] = 'missing'\nresults\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot({'good': 'data_good', 'bad': 'missing', 'also_good': 'data_also_good'})\n\n\nasync def test_repl_llm_conditional_external_call():\n    \"\"\"LLM only calls external function when a condition is met.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    call_count = 0\n\n    async def expensive_lookup(key: str) -> str:\n        nonlocal call_count\n        call_count += 1\n        return f'looked up {key}'\n\n    ext = {'expensive_lookup': expensive_lookup}\n\n    # Snippet 1: set up a cache\n    await run_repl_async(repl, \"cache = {'x': 'cached_x'}\", external_functions=ext)\n\n    # Snippet 2: LLM checks cache before calling\n    code = \"\"\"\\\nresults = []\nfor key in ['x', 'y', 'x']:\n    if key in cache:\n        results.append(cache[key])\n    else:\n        val = await expensive_lookup(key)\n        cache[key] = val\n        results.append(val)\nresults\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot(['cached_x', 'looked up y', 'cached_x'])\n    assert call_count == 1  # only 'y' triggered a call\n\n\nasync def test_repl_llm_side_effect_recording():\n    \"\"\"LLM uses a side-effect-only external function to record structured data.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    recorded: list[dict[str, object]] = []\n\n    def record_model(name: str, params: str, price: float) -> None:\n        recorded.append({'name': name, 'params': params, 'price': price})\n\n    async def get_models() -> list[dict[str, str]]:\n        return [\n            {'name': 'gpt-4', 'params': '1.7T'},\n            {'name': 'claude-3', 'params': '???'},\n        ]\n\n    ext = {'record_model': record_model, 'get_models': get_models}\n\n    code = \"\"\"\\\nmodels = await get_models()\nfor m in models:\n    record_model(m['name'], m['params'], 0.01)\nlen(models)\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot(2)\n    assert recorded == snapshot(\n        [{'name': 'gpt-4', 'params': '1.7T', 'price': 0.01}, {'name': 'claude-3', 'params': '???', 'price': 0.01}]\n    )\n\n\nasync def test_repl_llm_helper_wrapping_externals_with_retry():\n    \"\"\"LLM defines a helper function that wraps external calls with retry logic.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    attempt_counts: dict[str, int] = {}\n\n    def unreliable_fetch(url: str) -> str:\n        attempt_counts.setdefault(url, 0)\n        attempt_counts[url] += 1\n        if attempt_counts[url] < 2:\n            raise ValueError('temporary failure')\n        return f'content of {url}'\n\n    ext = {'unreliable_fetch': unreliable_fetch}\n\n    # Snippet 1: LLM defines retry helper\n    await run_repl_async(\n        repl,\n        \"\"\"\\\ndef fetch_with_retry(url, max_retries=3):\n    for i in range(max_retries):\n        try:\n            return unreliable_fetch(url)\n        except ValueError:\n            if i == max_retries - 1:\n                raise\n    raise ValueError('should not reach here')\n\"\"\",\n        external_functions=ext,\n    )\n\n    # Snippet 2: LLM uses the retry helper\n    result = await run_repl_async(repl, \"fetch_with_retry('example.com')\", external_functions=ext)\n    assert result == snapshot('content of example.com')\n    assert attempt_counts == snapshot({'example.com': 2})\n\n\nasync def test_repl_llm_nested_gather_with_sequential_deps():\n    \"\"\"LLM does gather of tasks where each task has sequential async steps internally.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def get_user(user_id: int) -> dict[str, object]:\n        return {'id': user_id, 'name': f'user_{user_id}'}\n\n    async def get_posts(user_id: int) -> list[str]:\n        return [f'post_{user_id}_1', f'post_{user_id}_2']\n\n    ext = {'get_user': get_user, 'get_posts': get_posts}\n\n    code = \"\"\"\\\nimport asyncio\n\nasync def get_user_with_posts(uid):\n    user = await get_user(uid)\n    posts = await get_posts(uid)\n    user['posts'] = posts\n    return user\n\nresults = await asyncio.gather(\n    get_user_with_posts(1),\n    get_user_with_posts(2),\n    get_user_with_posts(3),\n)\nresults\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot(\n        [\n            {'id': 1, 'name': 'user_1', 'posts': ['post_1_1', 'post_1_2']},\n            {'id': 2, 'name': 'user_2', 'posts': ['post_2_1', 'post_2_2']},\n            {'id': 3, 'name': 'user_3', 'posts': ['post_3_1', 'post_3_2']},\n        ]\n    )\n\n\nasync def test_repl_llm_external_returns_complex_nested_structure():\n    \"\"\"LLM processes deeply nested API response from external function.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def get_api_response() -> dict[str, object]:\n        return {\n            'status': 'ok',\n            'data': {\n                'users': [\n                    {'name': 'Alice', 'scores': [95, 87, 92]},\n                    {'name': 'Bob', 'scores': [78, 85, 90]},\n                ],\n                'metadata': {'page': 1, 'total': 2},\n            },\n        }\n\n    ext = {'get_api_response': get_api_response}\n\n    # Snippet 1: fetch and store\n    await run_repl_async(repl, 'response = await get_api_response()', external_functions=ext)\n\n    # Snippet 2: LLM navigates nested structure\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\nusers = response['data']['users']\naverages = {}\nfor u in users:\n    avg = sum(u['scores']) / len(u['scores'])\n    averages[u['name']] = round(avg, 1)\naverages\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot({'Alice': 91.3, 'Bob': 84.3})\n\n\nasync def test_repl_llm_external_with_kwargs():\n    \"\"\"LLM calls external functions using keyword arguments.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def search(query: str, limit: int = 10, offset: int = 0) -> dict[str, object]:\n        return {'query': query, 'limit': limit, 'offset': offset, 'results': [f'{query}_{i}' for i in range(limit)]}\n\n    ext = {'search': search}\n\n    code = \"\"\"\\\npage1 = await search('test', limit=2, offset=0)\npage2 = await search('test', limit=2, offset=2)\npage1['results'] + page2['results']\n\"\"\"\n    result = await run_repl_async(repl, code, external_functions=ext)\n    assert result == snapshot(['test_0', 'test_1', 'test_0', 'test_1'])\n\n\nasync def test_repl_llm_os_read_then_process_with_external():\n    \"\"\"LLM reads a file via OS, then processes content with an async external function.\"\"\"\n    from pydantic_monty import MemoryFile, OSAccess\n\n    repl = pydantic_monty.MontyRepl()\n    fs = OSAccess([MemoryFile('/data.csv', content='alice,95\\nbob,87\\ncharlie,92')])\n\n    async def analyze(text: str) -> dict[str, int]:\n        rows = text.strip().split('\\n')\n        return {name: int(score) for name, score in (r.split(',') for r in rows)}\n\n    ext = {'analyze': analyze}\n\n    # Snippet 1: read file\n    await run_repl_async(\n        repl,\n        \"\"\"\\\nfrom pathlib import Path\nraw = Path('/data.csv').read_text()\n\"\"\",\n        external_functions=ext,\n        os=fs,\n    )\n\n    # Snippet 2: process with external\n    result = await run_repl_async(repl, 'await analyze(raw)', external_functions=ext, os=fs)\n    assert result == snapshot({'alice': 95, 'bob': 87, 'charlie': 92})\n\n\nasync def test_repl_llm_long_multi_step_session():\n    \"\"\"Simulates a multi-step LLM agent session: setup, explore, process, summarize.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    db: dict[str, list[dict[str, object]]] = {\n        'products': [\n            {'name': 'Widget', 'price': 9.99, 'category': 'tools'},\n            {'name': 'Gadget', 'price': 24.99, 'category': 'electronics'},\n            {'name': 'Doohickey', 'price': 4.99, 'category': 'tools'},\n            {'name': 'Thingamajig', 'price': 49.99, 'category': 'electronics'},\n        ],\n    }\n\n    async def query_db(table: str, filters: dict[str, str] | None = None) -> list[dict[str, object]]:\n        rows = db.get(table, [])\n        if filters:\n            for k, v in filters.items():\n                rows = [r for r in rows if r.get(k) == v]\n        return rows\n\n    ext = {'query_db': query_db}\n\n    # Step 1: LLM explores what's available\n    result = await run_repl_async(repl, 'await query_db(\"products\")', external_functions=ext)\n    assert len(result) == 4\n\n    # Step 2: LLM filters by category\n    await run_repl_async(\n        repl,\n        \"tools = await query_db('products', filters={'category': 'tools'})\",\n        external_functions=ext,\n    )\n\n    # Step 3: LLM computes stats\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\ntotal = sum(p['price'] for p in tools)\navg = total / len(tools)\n{'count': len(tools), 'total': round(total, 2), 'average': round(avg, 2)}\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot({'count': 2, 'total': 14.98, 'average': 7.49})\n\n    # Step 4: LLM also checks electronics\n    await run_repl_async(\n        repl,\n        \"electronics = await query_db('products', filters={'category': 'electronics'})\",\n        external_functions=ext,\n    )\n\n    # Step 5: LLM builds final summary from accumulated state\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\nsummary = {}\nfor cat, items in [('tools', tools), ('electronics', electronics)]:\n    summary[cat] = {\n        'count': len(items),\n        'total': round(sum(i['price'] for i in items), 2),\n        'items': [i['name'] for i in items],\n    }\nsummary\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot(\n        {\n            'tools': {'count': 2, 'total': 14.98, 'items': ['Widget', 'Doohickey']},\n            'electronics': {'count': 2, 'total': 74.98, 'items': ['Gadget', 'Thingamajig']},\n        }\n    )\n\n\nasync def test_repl_llm_string_manipulation_of_external_result():\n    \"\"\"LLM fetches HTML-like content and does string processing across snippets.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    async def fetch_page(url: str) -> str:\n        return '<title>Test Page</title><body><p>Hello</p><p>World</p></body>'\n\n    ext = {'fetch_page': fetch_page}\n\n    await run_repl_async(repl, \"html = await fetch_page('example.com')\", external_functions=ext)\n\n    # LLM extracts title\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\nstart = html.find('<title>') + len('<title>')\nend = html.find('</title>')\ntitle = html[start:end]\ntitle\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot('Test Page')\n\n    # LLM extracts paragraphs\n    result = await run_repl_async(\n        repl,\n        \"\"\"\\\nparagraphs = []\nremaining = html\nwhile '<p>' in remaining:\n    s = remaining.find('<p>') + 3\n    e = remaining.find('</p>')\n    paragraphs.append(remaining[s:e])\n    remaining = remaining[e + 4:]\nparagraphs\n\"\"\",\n        external_functions=ext,\n    )\n    assert result == snapshot(['Hello', 'World'])\n\n\nasync def test_repl_llm_syntax_error_then_fix():\n    \"\"\"LLM writes code with a syntax error, then fixes it in the next snippet.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    ext = {'add': add}\n\n    # Snippet 1: set up state\n    await run_repl_async(repl, 'x = 10', external_functions=ext)\n\n    # Snippet 2: syntax error\n    with pytest.raises(pydantic_monty.MontySyntaxError):\n        await run_repl_async(repl, 'y = add(x,', external_functions=ext)\n\n    # Snippet 3: state preserved, LLM fixes the code\n    result = await run_repl_async(repl, 'y = add(x, 5)\\ny', external_functions=ext)\n    assert result == snapshot(15)\n"
  },
  {
    "path": "crates/monty-python/tests/test_basic.py",
    "content": "from inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_simple_expression():\n    m = pydantic_monty.Monty('1 + 2')\n    assert m.run() == snapshot(3)\n\n\ndef test_arithmetic():\n    m = pydantic_monty.Monty('10 * 5 - 3')\n    assert m.run() == snapshot(47)\n\n\ndef test_string_concatenation():\n    m = pydantic_monty.Monty('\"hello\" + \" \" + \"world\"')\n    assert m.run() == snapshot('hello world')\n\n\ndef test_multiple_runs_same_instance():\n    m = pydantic_monty.Monty('x * 2', inputs=['x'])\n    assert m.run(inputs={'x': 5}) == snapshot(10)\n    assert m.run(inputs={'x': 10}) == snapshot(20)\n    assert m.run(inputs={'x': -3}) == snapshot(-6)\n\n\ndef test_repr_no_inputs():\n    m = pydantic_monty.Monty('1 + 1')\n    assert repr(m) == snapshot(\"Monty(<1 line of code>, script_name='main.py')\")\n\n\ndef test_repr_with_inputs():\n    m = pydantic_monty.Monty('x', inputs=['x', 'y'])\n    assert repr(m) == snapshot('Monty(<1 line of code>, script_name=\\'main.py\\', inputs=[\"x\", \"y\"])')\n\n\ndef test_repr_with_external_functions():\n    m = pydantic_monty.Monty('foo()')\n    assert repr(m) == snapshot(\"Monty(<1 line of code>, script_name='main.py')\")\n\n\ndef test_repr_with_inputs_and_external_functions():\n    m = pydantic_monty.Monty('foo(x)', inputs=['x'])\n    assert repr(m) == snapshot('Monty(<1 line of code>, script_name=\\'main.py\\', inputs=[\"x\"])')\n\n\ndef test_multiline_code():\n    code = \"\"\"\nx = 1\ny = 2\nx + y\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    assert m.run() == snapshot(3)\n\n\ndef test_function_definition_and_call():\n    code = \"\"\"\ndef add(a, b):\n    return a + b\n\nadd(3, 4)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    assert m.run() == snapshot(7)\n"
  },
  {
    "path": "crates/monty-python/tests/test_dataclasses.py",
    "content": "from dataclasses import (\n    FrozenInstanceError,\n    asdict,\n    astuple,\n    dataclass,\n    fields,\n    is_dataclass,\n)\nfrom typing import NoReturn\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\n@dataclass\nclass Person:\n    name: str\n    age: int\n\n\ndef test_dataclass_input():\n    \"\"\"Dataclass instances are converted and returned as MontyDataclass.\"\"\"\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    m.register_dataclass(Person)\n    result = m.run(inputs={'x': Person(name='Alice', age=30)})\n    assert result.name == snapshot('Alice')\n    assert result.age == snapshot(30)\n    assert is_dataclass(result)\n    assert isinstance(result, Person)\n    assert asdict(result) == snapshot({'name': 'Alice', 'age': 30})\n    assert repr(result) == snapshot(\"Person(name='Alice', age=30)\")\n\n\ndef test_dataclass_auto_registered():\n    \"\"\"Dataclass passed as input is auto-registered, so isinstance() works without explicit registry.\"\"\"\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': Person(name='Alice', age=30)})\n    assert result.name == snapshot('Alice')\n    assert result.age == snapshot(30)\n    assert is_dataclass(result)\n    assert isinstance(result, Person)\n    assert asdict(result) == snapshot({'name': 'Alice', 'age': 30})\n    assert repr(result) == snapshot(\"Person(name='Alice', age=30)\")\n\n\n@dataclass(frozen=True)\nclass Point:\n    x: int\n    y: int\n\n\ndef test_dataclass_frozen():\n    \"\"\"Frozen dataclasses are converted like regular dataclasses.\"\"\"\n\n    m = pydantic_monty.Monty('p', inputs=['p'], dataclass_registry=[Point])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n    assert isinstance(result, Point)\n    assert result.x == snapshot(10)\n    assert result.y == snapshot(20)\n    assert repr(result) == snapshot('Point(x=10, y=20)')\n\n\n@dataclass\nclass Address:\n    city: str\n    zip_code: str\n\n\n@dataclass\nclass PersonAddress:\n    name: str\n    address: Address\n\n\ndef test_dataclass_nested():\n    \"\"\"Nested dataclasses are recursively converted.\"\"\"\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    m.register_dataclass(Address)\n    m.register_dataclass(PersonAddress)\n    result = m.run(inputs={'x': PersonAddress(name='Bob', address=Address(city='NYC', zip_code='10001'))})\n    assert isinstance(result, PersonAddress)\n    assert result.name == snapshot('Bob')\n    assert isinstance(result.address, Address)\n    assert result.address.city == snapshot('NYC')\n    assert result.address.zip_code == snapshot('10001')\n\n\ndef test_dataclass_nested_auto_registered():\n    \"\"\"Nested dataclasses are auto-registered when passed as input.\"\"\"\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': PersonAddress(name='Bob', address=Address(city='NYC', zip_code='10001'))})\n    assert isinstance(result, PersonAddress)\n    assert result.name == snapshot('Bob')\n    assert isinstance(result.address, Address)\n    assert result.address.city == snapshot('NYC')\n    assert result.address.zip_code == snapshot('10001')\n\n\ndef test_dataclass_auto_registered_in_list():\n    \"\"\"Dataclass inside a list input is auto-registered.\"\"\"\n\n    m = pydantic_monty.Monty('x[0]', inputs=['x'])\n    result = m.run(inputs={'x': [Person(name='Alice', age=30)]})\n    assert isinstance(result, Person)\n    assert result.name == snapshot('Alice')\n\n\ndef test_dataclass_auto_registered_in_dict_value():\n    \"\"\"Dataclass inside a dict value is auto-registered.\"\"\"\n\n    m = pydantic_monty.Monty('x[\"key\"]', inputs=['x'])\n    result = m.run(inputs={'x': {'key': Person(name='Alice', age=30)}})\n    assert isinstance(result, Person)\n    assert result.name == snapshot('Alice')\n\n\ndef test_dataclass_explicit_registry_idempotent():\n    \"\"\"Explicit registry still works alongside auto-registration (idempotent).\"\"\"\n\n    m = pydantic_monty.Monty('x', inputs=['x'], dataclass_registry=[Person])\n    result = m.run(inputs={'x': Person(name='Alice', age=30)})\n    assert isinstance(result, Person)\n    assert result.name == snapshot('Alice')\n    assert result.age == snapshot(30)\n\n\ndef test_dataclass_with_list_field():\n    \"\"\"Dataclasses with list fields are properly converted.\"\"\"\n\n    @dataclass\n    class Container:\n        items: list[int]\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': Container(items=[1, 2, 3])})\n    assert result.items == snapshot([1, 2, 3])\n\n\ndef test_dataclass_with_dict_field():\n    \"\"\"Dataclasses with dict fields are properly converted.\"\"\"\n\n    @dataclass\n    class Config:\n        settings: dict[str, int]\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    m.register_dataclass(Config)\n    result = m.run(inputs={'x': Config(settings={'a': 1, 'b': 2})})\n    assert result.settings == snapshot({'a': 1, 'b': 2})\n\n\ndef test_dataclass_empty():\n    \"\"\"Empty dataclass (no fields) has empty repr.\"\"\"\n\n    @dataclass\n    class Empty:\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    m.register_dataclass(Empty)\n    result = m.run(inputs={'x': Empty()})\n    assert repr(result) == snapshot('test_dataclass_empty.<locals>.Empty()')\n\n\n@pytest.mark.xfail(reason='We should extend the dataclass registry to cover all types, then test it is enforced')\ndef test_dataclass_type_raises():\n    \"\"\"Dataclass type (not instance) should raise TypeError.\"\"\"\n\n    @dataclass\n    class MyClass:\n        value: int\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    m.register_dataclass(MyClass)\n    with pytest.raises(TypeError) as exc_info:\n        m.run(inputs={'x': MyClass})\n\n    assert str(exc_info.value) == snapshot('Cannot convert builtins.type to Monty value')\n\n\n# === Field access ===\n\n\ndef test_dataclass_field_access():\n    \"\"\"Access individual fields of a dataclass.\"\"\"\n\n    @dataclass\n    class Person:\n        name: str\n        age: int\n\n    m = pydantic_monty.Monty('x.name', inputs=['x'])\n    assert m.run(inputs={'x': Person(name='Alice', age=30)}) == snapshot('Alice')\n\n    m = pydantic_monty.Monty('x.age', inputs=['x'])\n    assert m.run(inputs={'x': Person(name='Alice', age=30)}) == snapshot(30)\n\n\ndef test_dataclass_field_access_nested():\n    \"\"\"Access fields of nested dataclasses.\"\"\"\n\n    m = pydantic_monty.Monty('x.address.city', inputs=['x'])\n    result = m.run(inputs={'x': PersonAddress(name='Bob', address=Address(city='NYC', zip_code='10001'))})\n    assert result == snapshot('NYC')\n\n\ndef test_dataclass_field_in_expression():\n    \"\"\"Use dataclass fields in expressions.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p.x + p.y', inputs=['p'])\n    assert m.run(inputs={'p': Point(x=10, y=20)}) == snapshot(30)\n\n\ndef test_dataclass_field_access_missing():\n    \"\"\"Accessing a non-existent field raises AttributeError.\"\"\"\n\n    @dataclass\n    class Person:\n        name: str\n\n    m = pydantic_monty.Monty('x.age', inputs=['x'])\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(inputs={'x': Person(name='Alice')})\n    assert isinstance(exc_info.value.exception(), AttributeError)\n\n\n# === Repr ===\n\n\ndef test_dataclass_repr():\n    \"\"\"Repr of dataclass shows ClassName(field=value, ...) format.\"\"\"\n\n    @dataclass\n    class Person:\n        name: str\n        age: int\n\n    m = pydantic_monty.Monty('repr(x)', inputs=['x'])\n    assert m.run(inputs={'x': Person(name='Alice', age=30)}) == snapshot(\"Person(name='Alice', age=30)\")\n\n\ndef test_dataclass_repr_frozen():\n    \"\"\"Repr of frozen dataclass shows same format.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('repr(p)', inputs=['p'])\n    assert m.run(inputs={'p': Point(x=10, y=20)}) == snapshot('Point(x=10, y=20)')\n\n\ndef test_dataclass_repr_nested():\n    \"\"\"Repr of nested dataclass shows nested repr.\"\"\"\n\n    @dataclass\n    class Inner:\n        value: int\n\n    @dataclass\n    class Outer:\n        inner: Inner\n\n    m = pydantic_monty.Monty('repr(x)', inputs=['x'])\n    assert m.run(inputs={'x': Outer(inner=Inner(value=42))}) == snapshot('Outer(inner=Inner(value=42))')\n\n\ndef test_dataclass_repr_empty():\n    \"\"\"Repr of empty dataclass shows ClassName().\"\"\"\n\n    @dataclass\n    class Empty:\n        pass\n\n    m = pydantic_monty.Monty('repr(x)', inputs=['x'])\n    m.register_dataclass(Empty)\n    assert m.run(inputs={'x': Empty()}) == snapshot('Empty()')\n\n\n# === Setattr ===\n\n\ndef test_dataclass_setattr_mutable():\n    \"\"\"Setting attributes on mutable dataclass works (auto-registered, returns real dataclass).\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n    assert isinstance(result, Point)\n\n    # Modify existing field\n    result.x = 100\n    assert result.x == snapshot(100)\n    assert repr(result) == snapshot('test_dataclass_setattr_mutable.<locals>.Point(x=100, y=20)')\n\n\ndef test_dataclass_setattr_frozen():\n    \"\"\"Setting attributes on frozen dataclass raises FrozenInstanceError.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    # FrozenInstanceError is raised (which is a subclass of AttributeError)\n    with pytest.raises(FrozenInstanceError, match=\"cannot assign to field 'x'\"):\n        result.x = 100\n\n    with pytest.raises(FrozenInstanceError, match=\"cannot assign to field 'z'\"):\n        result.z = 30\n\n\ndef test_frozen_instance_error_is_attribute_error():\n    \"\"\"FrozenInstanceError can be caught as AttributeError.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    # Can catch with AttributeError (parent class)\n    with pytest.raises(AttributeError):\n        result.x = 100\n\n    # Verify it's actually FrozenInstanceError\n    try:\n        result.y = 200\n    except AttributeError as e:\n        assert isinstance(e, FrozenInstanceError)\n\n\ndef test_frozen_instance_error_message():\n    \"\"\"FrozenInstanceError has correct message format.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    with pytest.raises(FrozenInstanceError) as exc_info:\n        result.x = 100\n    assert exc_info.value.args[0] == snapshot(\"cannot assign to field 'x'\")\n\n\ndef test_frozen_instance_error_from_monty_code():\n    \"\"\"FrozenInstanceError raised by Monty code is properly converted.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    # Monty code that tries to modify a frozen dataclass\n    code = \"\"\"\np.x = 100\n\"\"\"\n    m = pydantic_monty.Monty(code, inputs=['p'])\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(inputs={'p': Point(x=10, y=20)})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, FrozenInstanceError)\n    assert inner.args[0] == snapshot(\"cannot assign to field 'x'\")\n\n\ndef test_frozen_instance_error_from_monty_caught_as_attribute_error():\n    \"\"\"FrozenInstanceError from Monty can be caught as AttributeError.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    code = 'p.x = 100'\n    m = pydantic_monty.Monty(code, inputs=['p'])\n\n    # Wrapped in MontyRuntimeError, but inner exception is FrozenInstanceError\n    # which is a subclass of AttributeError\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(inputs={'p': Point(x=10, y=20)})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, AttributeError)\n    assert isinstance(inner, FrozenInstanceError)\n\n\ndef test_frozen_instance_error_from_external_function():\n    \"\"\"FrozenInstanceError from external function is properly converted.\"\"\"\n    code = \"\"\"\ntry:\n    fail()\nexcept FrozenInstanceError:\n    caught = 'frozen'\nexcept AttributeError:\n    caught = 'attr'\ncaught\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def fail() -> NoReturn:\n        raise FrozenInstanceError('cannot assign to field')\n\n    # Monty should catch it as FrozenInstanceError specifically\n    result = m.run(external_functions={'fail': fail})\n    assert result == snapshot('frozen')\n\n\ndef test_frozen_instance_error_from_external_function_propagates():\n    \"\"\"FrozenInstanceError from external function propagates to Python.\"\"\"\n    m = pydantic_monty.Monty('fail()')\n\n    def fail() -> NoReturn:\n        raise FrozenInstanceError('test frozen error')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'fail': fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, FrozenInstanceError)\n    assert inner.args[0] == snapshot('test frozen error')\n\n\n# === Equality ===\n\n\ndef test_dataclass_equality_same():\n    \"\"\"Equal dataclasses compare equal.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(a, b)', inputs=['a', 'b'])\n    a, b = m.run(inputs={'a': Point(x=10, y=20), 'b': Point(x=10, y=20)})\n    assert a == b\n\n\ndef test_dataclass_equality_different_values():\n    \"\"\"Dataclasses with different values compare not equal.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(a, b)', inputs=['a', 'b'])\n    a, b = m.run(inputs={'a': Point(x=10, y=20), 'b': Point(x=10, y=30)})\n    assert a != b\n\n\ndef test_dataclass_equality_different_types():\n    \"\"\"Dataclasses of different types compare not equal.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    @dataclass\n    class Vector:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(a, b)', inputs=['a', 'b'])\n    a, b = m.run(inputs={'a': Point(x=10, y=20), 'b': Vector(x=10, y=20)})\n    assert a != b\n\n\ndef test_dataclass_equality_with_other_type():\n    \"\"\"Dataclass compared to non-dataclass returns False.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n    assert result != {'x': 10, 'y': 20}\n    assert result != (10, 20)\n    assert result != 'Point(x=10, y=20)'\n\n\n# === Hashing ===\n\n\ndef test_dataclass_hash_frozen():\n    \"\"\"Frozen dataclasses are hashable.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    h = hash(result)\n    assert isinstance(h, int)\n    # Hash is consistent\n    assert hash(result) == h\n\n\ndef test_dataclass_hash_frozen_equal_values():\n    \"\"\"Equal frozen dataclasses have equal hashes.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(a, b)', inputs=['a', 'b'])\n    a, b = m.run(inputs={'a': Point(x=10, y=20), 'b': Point(x=10, y=20)})\n\n    assert hash(a) == hash(b)\n\n\ndef test_dataclass_hash_mutable_raises():\n    \"\"\"Mutable dataclasses are not hashable.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    with pytest.raises(TypeError, match=\"unhashable type: 'Point'\"):\n        hash(result)\n\n\ndef test_dataclass_hash_in_set():\n    \"\"\"Frozen dataclasses can be used in sets.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(a, b, c)', inputs=['a', 'b', 'c'])\n    a, b, c = m.run(\n        inputs={\n            'a': Point(x=10, y=20),\n            'b': Point(x=10, y=20),  # duplicate\n            'c': Point(x=30, y=40),\n        }\n    )\n\n    s = {a, b, c}\n    assert len(s) == snapshot(2)\n\n\ndef test_dataclass_hash_as_dict_key():\n    \"\"\"Frozen dataclasses can be used as dict keys.\"\"\"\n\n    @dataclass(frozen=True)\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(a, b)', inputs=['a', 'b'])\n    a, b = m.run(inputs={'a': Point(x=10, y=20), 'b': Point(x=10, y=20)})\n\n    d = {a: 'first'}\n    assert d[b] == snapshot('first')\n\n\n# === dataclasses module compatibility ===\n\n\ndef test_dataclass_is_dataclass():\n    \"\"\"is_dataclass() returns True for returned dataclasses.\"\"\"\n\n    @dataclass\n    class Person:\n        name: str\n        age: int\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': Person(name='Alice', age=30)})\n    assert is_dataclass(result) is True\n\n\ndef test_dataclass_fields():\n    \"\"\"fields() returns Field objects for returned dataclasses.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    fs = fields(result)\n    assert len(fs) == snapshot(2)\n    assert fs[0].name == snapshot('x')\n    assert fs[1].name == snapshot('y')\n    # Type is inferred from value\n    assert fs[0].type is int\n    assert fs[1].type is int\n\n\ndef test_dataclass_fields_string():\n    \"\"\"fields() returns correct type for string fields.\"\"\"\n\n    @dataclass\n    class Person:\n        name: str\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Person(name='Alice')})\n\n    fs = fields(result)\n    assert fs[0].name == snapshot('name')\n    assert fs[0].type is str\n\n\ndef test_dataclass_asdict():\n    \"\"\"asdict() converts returned dataclass to dict.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    d = asdict(result)\n    assert d == snapshot({'x': 10, 'y': 20})\n\n\ndef test_dataclass_asdict_nested():\n    \"\"\"asdict() recursively converts nested dataclasses.\"\"\"\n\n    @dataclass\n    class Inner:\n        value: int\n\n    @dataclass\n    class Outer:\n        inner: Inner\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': Outer(inner=Inner(value=42))})\n\n    d = asdict(result)\n    assert d == snapshot({'inner': {'value': 42}})\n\n\ndef test_dataclass_astuple():\n    \"\"\"astuple() converts returned dataclass to tuple.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    t = astuple(result)\n    assert t == snapshot((10, 20))\n\n\ndef test_dataclass_dataclass_fields_attr():\n    \"\"\"__dataclass_fields__ attribute is accessible.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    df = result.__dataclass_fields__\n    assert 'x' in df\n    assert 'y' in df\n    assert df['x'].name == snapshot('x')\n    assert df['y'].name == snapshot('y')\n\n\ndef test_dataclass_params_frozen():\n    \"\"\"__dataclass_params__.frozen reflects frozen status.\"\"\"\n\n    @dataclass(frozen=True)\n    class FrozenPoint:\n        x: int\n        y: int\n\n    @dataclass\n    class MutablePoint:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('(f, m)', inputs=['f', 'm'])\n    frozen, mutable = m.run(inputs={'f': FrozenPoint(x=1, y=2), 'm': MutablePoint(x=3, y=4)})\n\n    assert frozen.__dataclass_params__.frozen is True\n    assert mutable.__dataclass_params__.frozen is False\n\n\ndef test_dataclass_params_attributes():\n    \"\"\"__dataclass_params__ has expected attributes.\"\"\"\n\n    @dataclass\n    class Point:\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Point(x=10, y=20)})\n\n    params = result.__dataclass_params__\n    assert params.init is True\n    assert params.repr is True\n    assert params.eq is True\n    assert params.order is False\n    assert params.frozen is False\n\n\ndef test_repeat_dataclass_name():\n    \"\"\"Two classes with the same name are distinguished because we use id, not name.\"\"\"\n\n    def create_point():\n        @dataclass\n        class Point:\n            x: int\n            y: int\n\n        return Point\n\n    point_cls2 = create_point()\n    m = pydantic_monty.Monty('a, b', inputs=['a', 'b'], dataclass_registry=[Point, point_cls2])\n    a, b = m.run(inputs={'a': Point(x=10, y=20), 'b': point_cls2(x=30, y=40)})\n    assert isinstance(a, Point)\n    assert isinstance(b, point_cls2)\n\n\n# === Dataclass method call tests ===\n\n\n@dataclass\nclass Greeter:\n    greeting: str\n\n    def greet(self) -> str:\n        return self.greeting\n\n\n@dataclass\nclass Calculator:\n    value: int\n\n    def add(self, n: int) -> int:\n        return self.value + n\n\n    def multiply(self, n: int) -> int:\n        return self.value * n\n\n\n@dataclass\nclass Point2D:\n    x: float\n    y: float\n\n    def distance(self) -> float:\n        return (self.x**2 + self.y**2) ** 0.5\n\n    def translate(self, dx: float, dy: float) -> 'Point2D':\n        return Point2D(x=self.x + dx, y=self.y + dy)\n\n\ndef test_method_no_args_raw():\n    \"\"\"Calling a dataclass method with no args (besides self), raw.\"\"\"\n    m = pydantic_monty.Monty('g.greet()', inputs=['g'], dataclass_registry=[Greeter])\n    result = m.start(inputs={'g': Greeter(greeting='hello')})\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.script_name == snapshot('main.py')\n    assert result.function_name == snapshot('greet')\n    assert result.args == snapshot((Greeter(greeting='hello'),))\n    assert result.kwargs == snapshot({})\n\n\ndef test_method_no_args():\n    \"\"\"Calling a dataclass method with no args (besides self).\"\"\"\n    m = pydantic_monty.Monty('g.greet()', inputs=['g'], dataclass_registry=[Greeter])\n    result = m.run(inputs={'g': Greeter(greeting='hello')})\n    assert result == snapshot('hello')\n\n\ndef test_method_with_args():\n    \"\"\"Calling a dataclass method with positional args.\"\"\"\n    m = pydantic_monty.Monty('c.add(10)', inputs=['c'], dataclass_registry=[Calculator])\n    result = m.run(inputs={'c': Calculator(value=5)})\n    assert result == snapshot(15)\n\n\ndef test_method_accessing_fields():\n    \"\"\"Method that reads multiple fields from self.\"\"\"\n    m = pydantic_monty.Monty('p.distance()', inputs=['p'], dataclass_registry=[Point2D])\n    result = m.run(inputs={'p': Point2D(x=3.0, y=4.0)})\n    assert result == snapshot(5.0)\n\n\ndef test_method_returning_dataclass():\n    \"\"\"Method that returns a new dataclass instance.\"\"\"\n    m = pydantic_monty.Monty('p.translate(1.0, 2.0)', inputs=['p'], dataclass_registry=[Point2D])\n    result = m.run(inputs={'p': Point2D(x=3.0, y=4.0)})\n    assert isinstance(result, Point2D)\n    assert result.x == snapshot(4.0)\n    assert result.y == snapshot(6.0)\n\n\ndef test_method_on_frozen_dataclass():\n    \"\"\"Methods work on frozen dataclasses too.\"\"\"\n\n    @dataclass(frozen=True)\n    class FrozenCalc:\n        value: int\n\n        def doubled(self) -> int:\n            return self.value * 2\n\n    m = pydantic_monty.Monty('c.doubled()', inputs=['c'], dataclass_registry=[FrozenCalc])\n    result = m.run(inputs={'c': FrozenCalc(value=21)})\n    assert result == snapshot(42)\n\n\ndef test_method_with_kwargs():\n    \"\"\"Method called with keyword arguments.\"\"\"\n\n    @dataclass\n    class Formatter:\n        base: str\n\n        def format(self, prefix: str = '', suffix: str = '') -> str:\n            return prefix + self.base + suffix\n\n    m = pydantic_monty.Monty(\n        \"f.format(prefix='[', suffix=']')\",\n        inputs=['f'],\n        dataclass_registry=[Formatter],\n    )\n    result = m.run(inputs={'f': Formatter(base='hello')})\n    assert result == snapshot('[hello]')\n\n\ndef test_method_multiple_calls():\n    \"\"\"Multiple method calls in the same expression.\"\"\"\n    m = pydantic_monty.Monty(\n        'c.add(10) + c.multiply(3)',\n        inputs=['c'],\n        dataclass_registry=[Calculator],\n    )\n    result = m.run(inputs={'c': Calculator(value=5)})\n    assert result == snapshot(30)\n\n\ndef test_method_nonexistent_raises():\n    \"\"\"Calling a non-existent method raises AttributeError.\"\"\"\n    m = pydantic_monty.Monty('g.nonexistent()', inputs=['g'], dataclass_registry=[Greeter])\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(inputs={'g': Greeter(greeting='hi')})\n    assert str(exc_info.value) == snapshot(\"AttributeError: 'Greeter' object has no attribute 'nonexistent'\")\n\n\ndef test_method_on_nested_dataclass_in_list():\n    \"\"\"Method call on a dataclass nested inside a list input.\"\"\"\n    m = pydantic_monty.Monty('items[0].greet()', inputs=['items'], dataclass_registry=[Greeter])\n    result = m.run(inputs={'items': [Greeter(greeting='nested')]})\n    assert result == snapshot('nested')\n\n\ndef test_method_on_nested_dataclass_in_dict():\n    \"\"\"Method call on a dataclass nested inside a dict input.\"\"\"\n    m = pydantic_monty.Monty('d[\"g\"].greet()', inputs=['d'], dataclass_registry=[Greeter])\n    result = m.run(inputs={'d': {'g': Greeter(greeting='from dict')}})\n    assert result == snapshot('from dict')\n\n\ndef test_method_on_nested_dataclass_in_tuple():\n    \"\"\"Method call on a dataclass nested inside a tuple input.\"\"\"\n    m = pydantic_monty.Monty('t[1].add(10)', inputs=['t'], dataclass_registry=[Calculator])\n    result = m.run(inputs={'t': (0, Calculator(value=5))})\n    assert result == snapshot(15)\n\n\ndef test_dataclass_private_fields_skipped():\n    \"\"\"Private fields (starting with _) are excluded from conversion.\"\"\"\n\n    @dataclass\n    class WithPrivate:\n        name: str\n        _internal: int = 0\n\n    m = pydantic_monty.Monty('repr(x)', inputs=['x'])\n    result = m.run(inputs={'x': WithPrivate(name='Alice', _internal=42)})\n    assert result == snapshot(\"WithPrivate(name='Alice')\")\n\n\ndef test_dataclass_private_fields_skipped_no_default():\n    \"\"\"Private fields without defaults cause TypeError on reconstruction (field is missing).\"\"\"\n\n    @dataclass\n    class WithPrivateNoDefault:\n        name: str\n        _secret: str\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    with pytest.raises(TypeError):\n        m.run(inputs={'x': WithPrivateNoDefault(name='Alice', _secret='hidden')})\n\n\ndef test_dataclass_private_field_not_accessible_in_monty():\n    \"\"\"Private fields are not accessible inside Monty expressions.\"\"\"\n\n    @dataclass\n    class WithPrivate:\n        name: str\n        _internal: int = 0\n\n    m = pydantic_monty.Monty('x._internal', inputs=['x'])\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(inputs={'x': WithPrivate(name='Alice', _internal=42)})\n    assert isinstance(exc_info.value.exception(), AttributeError)\n\n\ndef test_method_on_nested_dataclass_field():\n    \"\"\"Method call on a dataclass that is a field of another dataclass (d.c.method()).\"\"\"\n\n    @dataclass\n    class Inner:\n        value: int\n\n        def doubled(self) -> int:\n            return self.value * 2\n\n    @dataclass\n    class Outer:\n        inner: Inner\n\n    m = pydantic_monty.Monty('o.inner.doubled()', inputs=['o'], dataclass_registry=[Outer, Inner])\n    result = m.run(inputs={'o': Outer(inner=Inner(value=21))})\n    assert result == snapshot(42)\n"
  },
  {
    "path": "crates/monty-python/tests/test_exceptions.py",
    "content": "import pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n# === MontyRuntimeError tests ===\n\n\ndef test_zero_division_error():\n    m = pydantic_monty.Monty('1 / 0')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    # Check that it's also a MontyError\n    assert isinstance(exc_info.value, pydantic_monty.MontyError)\n    # Check the inner exception\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ZeroDivisionError)\n\n\ndef test_value_error():\n    m = pydantic_monty.Monty(\"raise ValueError('bad value')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert str(inner) == snapshot('bad value')\n\n\ndef test_type_error():\n    m = pydantic_monty.Monty(\"'string' + 1\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, TypeError)\n\n\ndef test_index_error():\n    m = pydantic_monty.Monty('[1, 2, 3][10]')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, IndexError)\n\n\ndef test_key_error():\n    m = pydantic_monty.Monty(\"{'a': 1}['b']\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, KeyError)\n\n\ndef test_attribute_error():\n    m = pydantic_monty.Monty(\"raise AttributeError('no such attr')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, AttributeError)\n    assert str(inner) == snapshot('no such attr')\n\n\ndef test_name_error():\n    m = pydantic_monty.Monty('undefined_variable')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, NameError)\n\n\ndef test_assertion_error():\n    m = pydantic_monty.Monty('assert False')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, AssertionError)\n\n\ndef test_assertion_error_with_message():\n    m = pydantic_monty.Monty(\"assert False, 'custom message'\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, AssertionError)\n    assert str(inner) == snapshot('custom message')\n\n\ndef test_runtime_error():\n    m = pydantic_monty.Monty(\"raise RuntimeError('runtime error')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, RuntimeError)\n    assert str(inner) == snapshot('runtime error')\n\n\ndef test_not_implemented_error():\n    m = pydantic_monty.Monty(\"raise NotImplementedError('not implemented')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, NotImplementedError)\n    assert str(inner) == snapshot('not implemented')\n\n\n# === MontySyntaxError tests ===\n\n\ndef test_syntax_error_on_init():\n    with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:\n        pydantic_monty.Monty('def')\n    # Check that it's also a MontyError\n    assert isinstance(exc_info.value, pydantic_monty.MontyError)\n    # Check the inner exception\n    inner = exc_info.value.exception()\n    assert isinstance(inner, SyntaxError)\n\n\ndef test_syntax_error_unclosed_paren():\n    with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:\n        pydantic_monty.Monty('print(1')\n    inner = exc_info.value.exception()\n    assert isinstance(inner, SyntaxError)\n\n\ndef test_syntax_error_invalid_syntax():\n    with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:\n        pydantic_monty.Monty('x = = 1')\n    inner = exc_info.value.exception()\n    assert isinstance(inner, SyntaxError)\n\n\n# === Catching with base class ===\n\n\ndef test_catch_with_base_class():\n    m = pydantic_monty.Monty('1 / 0')\n    with pytest.raises(pydantic_monty.MontyError):\n        m.run()\n\n\ndef test_catch_syntax_error_with_base_class():\n    with pytest.raises(pydantic_monty.MontyError):\n        pydantic_monty.Monty('def')\n\n\n# === Exception handling within Monty ===\n\n\ndef test_raise_caught_exception():\n    code = \"\"\"\ntry:\n    1 / 0\nexcept ZeroDivisionError as e:\n    result = 'caught'\nresult\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    assert m.run() == snapshot('caught')\n\n\ndef test_exception_in_function():\n    code = \"\"\"\ndef fail():\n    raise ValueError('from function')\n\nfail()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert str(inner) == snapshot('from function')\n\n\n# === Display and str methods ===\n\n\ndef test_display_traceback():\n    m = pydantic_monty.Monty('1 / 0')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    display = exc_info.value.display()\n    assert 'Traceback (most recent call last):' in display\n    assert 'ZeroDivisionError' in display\n\n\ndef test_display_type_msg():\n    m = pydantic_monty.Monty(\"raise ValueError('test message')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    display = exc_info.value.display('type-msg')\n    assert display == snapshot('ValueError: test message')\n\n\ndef test_runtime_display():\n    m = pydantic_monty.Monty(\"raise ValueError('test message')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    assert exc_info.value.display('msg') == snapshot('test message')\n    assert exc_info.value.display('type-msg') == snapshot('ValueError: test message')\n    assert exc_info.value.display() == snapshot(\"\"\"\\\nTraceback (most recent call last):\n  File \"main.py\", line 1, in <module>\n    raise ValueError('test message')\nValueError: test message\\\n\"\"\")\n\n\ndef test_str_returns_msg():\n    m = pydantic_monty.Monty(\"raise ValueError('test message')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    assert str(exc_info.value) == snapshot('ValueError: test message')\n\n\ndef test_syntax_error_display():\n    with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:\n        pydantic_monty.Monty('def')\n    assert exc_info.value.display() == snapshot('Expected an identifier at byte range 3..3')\n    assert exc_info.value.display('type-msg') == snapshot('SyntaxError: Expected an identifier at byte range 3..3')\n\n\ndef test_syntax_error_str():\n    with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:\n        pydantic_monty.Monty('def')\n    # str() returns just the message\n    assert 'SyntaxError' not in str(exc_info.value)\n\n\n# === Traceback tests ===\n\n\ndef test_traceback_frames():\n    code = \"\"\"\\\ndef inner():\n    raise ValueError('error')\n\ndef outer():\n    inner()\n\nouter()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    frames = exc_info.value.traceback()\n    assert isinstance(frames, list)\n    assert len(frames) >= 2  # At least module level, outer(), and inner()\n\n    assert exc_info.value.display() == snapshot(\"\"\"\\\nTraceback (most recent call last):\n  File \"main.py\", line 7, in <module>\n    outer()\n    ~~~~~~~\n  File \"main.py\", line 5, in outer\n    inner()\n    ~~~~~~~\n  File \"main.py\", line 2, in inner\n    raise ValueError('error')\nValueError: error\\\n\"\"\")\n\n    assert [f.dict() for f in frames] == snapshot(\n        [\n            {\n                'filename': 'main.py',\n                'line': 7,\n                'column': 1,\n                'end_line': 7,\n                'end_column': 8,\n                'function_name': '<module>',\n                'source_line': 'outer()',\n            },\n            {\n                'filename': 'main.py',\n                'line': 5,\n                'column': 5,\n                'end_line': 5,\n                'end_column': 12,\n                'function_name': 'outer',\n                'source_line': '    inner()',\n            },\n            {\n                'filename': 'main.py',\n                'line': 2,\n                'column': 11,\n                'end_line': 2,\n                'end_column': 30,\n                'function_name': 'inner',\n                'source_line': \"    raise ValueError('error')\",\n            },\n        ]\n    )\n\n\ndef test_frame_properties():\n    code = \"\"\"\ndef foo():\n    raise ValueError('test')\n\nfoo()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    frames = exc_info.value.traceback()\n\n    assert [f.dict() for f in frames] == snapshot(\n        [\n            {\n                'filename': 'main.py',\n                'line': 5,\n                'column': 1,\n                'end_line': 5,\n                'end_column': 6,\n                'function_name': '<module>',\n                'source_line': 'foo()',\n            },\n            {\n                'filename': 'main.py',\n                'line': 3,\n                'column': 11,\n                'end_line': 3,\n                'end_column': 29,\n                'function_name': 'foo',\n                'source_line': \"    raise ValueError('test')\",\n            },\n        ]\n    )\n\n\n# === Repr tests ===\n\n\ndef test_runtime_error_repr():\n    m = pydantic_monty.Monty(\"raise ValueError('test')\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    assert repr(exc_info.value) == snapshot('MontyRuntimeError(ValueError: test)')\n\n\ndef test_syntax_error_repr():\n    with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:\n        pydantic_monty.Monty('def')\n    assert repr(exc_info.value) == snapshot('MontySyntaxError(Expected an identifier at byte range 3..3)')\n\n\ndef test_frame_repr():\n    code = \"\"\"\ndef foo():\n    raise ValueError('test')\n\nfoo()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    frames = exc_info.value.traceback()\n    frame = frames[0]\n    assert repr(frame) == snapshot(\"Frame(filename='main.py', line=5, column=1, function_name='<module>')\")\n"
  },
  {
    "path": "crates/monty-python/tests/test_external.py",
    "content": "from typing import Any\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_external_function_no_args():\n    m = pydantic_monty.Monty('noop()')\n\n    def noop(*args: Any, **kwargs: Any) -> str:\n        assert args == snapshot(())\n        assert kwargs == snapshot({})\n        return 'called'\n\n    assert m.run(external_functions={'noop': noop}) == snapshot('called')\n\n\ndef test_external_function_positional_args():\n    m = pydantic_monty.Monty('func(1, 2, 3)')\n\n    def func(*args: Any, **kwargs: Any) -> str:\n        assert args == snapshot((1, 2, 3))\n        assert kwargs == snapshot({})\n        return 'ok'\n\n    assert m.run(external_functions={'func': func}) == snapshot('ok')\n\n\ndef test_external_function_kwargs_only():\n    m = pydantic_monty.Monty('func(a=1, b=\"two\")')\n\n    def func(*args: Any, **kwargs: Any) -> str:\n        assert args == snapshot(())\n        assert kwargs == snapshot({'a': 1, 'b': 'two'})\n        return 'ok'\n\n    assert m.run(external_functions={'func': func}) == snapshot('ok')\n\n\ndef test_external_function_mixed_args_kwargs():\n    m = pydantic_monty.Monty('func(1, 2, x=\"hello\", y=True)')\n\n    def func(*args: Any, **kwargs: Any) -> str:\n        assert args == snapshot((1, 2))\n        assert kwargs == snapshot({'x': 'hello', 'y': True})\n        return 'ok'\n\n    assert m.run(external_functions={'func': func}) == snapshot('ok')\n\n\ndef test_external_function_complex_types():\n    m = pydantic_monty.Monty('func([1, 2], {\"key\": \"value\"})')\n\n    def func(*args: Any, **kwargs: Any) -> str:\n        assert args == snapshot(([1, 2], {'key': 'value'}))\n        assert kwargs == snapshot({})\n        return 'ok'\n\n    assert m.run(external_functions={'func': func}) == snapshot('ok')\n\n\ndef test_external_function_returns_none():\n    m = pydantic_monty.Monty('do_nothing()')\n\n    def do_nothing(*args: Any, **kwargs: Any) -> None:\n        assert args == snapshot(())\n        assert kwargs == snapshot({})\n\n    assert m.run(external_functions={'do_nothing': do_nothing}) is None\n\n\ndef test_external_function_returns_complex_type():\n    m = pydantic_monty.Monty('get_data()')\n\n    def get_data(*args: Any, **kwargs: Any) -> dict[str, Any]:\n        return {'a': [1, 2, 3], 'b': {'nested': True}}\n\n    result = m.run(external_functions={'get_data': get_data})\n    assert result == snapshot({'a': [1, 2, 3], 'b': {'nested': True}})\n\n\ndef test_multiple_external_functions():\n    m = pydantic_monty.Monty('add(1, 2) + mul(3, 4)')\n\n    def add(*args: Any, **kwargs: Any) -> int:\n        assert args == snapshot((1, 2))\n        assert kwargs == snapshot({})\n        return args[0] + args[1]\n\n    def mul(*args: Any, **kwargs: Any) -> int:\n        assert args == snapshot((3, 4))\n        assert kwargs == snapshot({})\n        return args[0] * args[1]\n\n    result = m.run(external_functions={'add': add, 'mul': mul})\n    assert result == snapshot(15)  # 3 + 12\n\n\ndef test_external_function_called_multiple_times():\n    m = pydantic_monty.Monty('counter() + counter() + counter()')\n\n    call_count = 0\n\n    def counter(*args: Any, **kwargs: Any) -> int:\n        nonlocal call_count\n        assert args == snapshot(())\n        assert kwargs == snapshot({})\n        call_count += 1\n        return call_count\n\n    result = m.run(external_functions={'counter': counter})\n    assert result == snapshot(6)  # 1 + 2 + 3\n    assert call_count == snapshot(3)\n\n\ndef test_external_function_with_input():\n    m = pydantic_monty.Monty('process(x)', inputs=['x'])\n\n    def process(*args: Any, **kwargs: Any) -> int:\n        assert args == snapshot((5,))\n        assert kwargs == snapshot({})\n        return args[0] * 10\n\n    assert m.run(inputs={'x': 5}, external_functions={'process': process}) == snapshot(50)\n\n\ndef test_external_function_not_provided_raises_name_error():\n    \"\"\"Calling an unknown function without external_functions raises NameError.\"\"\"\n    m = pydantic_monty.Monty('missing()')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert type(inner) is NameError\n    assert str(inner) == snapshot(\"name 'missing' is not defined\")\n\n\ndef test_undeclared_function_raises_name_error():\n    m = pydantic_monty.Monty('unknown_func()')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    inner = exc_info.value.exception()\n    assert type(inner) is NameError\n    assert str(inner) == snapshot(\"name 'unknown_func' is not defined\")\n\n\ndef test_external_function_raises_exception():\n    \"\"\"Test that exceptions from external functions propagate to the caller.\"\"\"\n    m = pydantic_monty.Monty('fail()')\n\n    def fail(*args: Any, **kwargs: Any) -> None:\n        raise ValueError('intentional error')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'fail': fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('intentional error')\n\n\ndef test_external_function_wrong_name_raises():\n    \"\"\"Test that calling a function not in external_functions raises NameError.\"\"\"\n    m = pydantic_monty.Monty('foo()')\n\n    def bar(*args: Any, **kwargs: Any) -> int:\n        return 1\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'bar': bar})\n    inner = exc_info.value.exception()\n    assert type(inner) is NameError\n    assert str(inner) == snapshot(\"name 'foo' is not defined\")\n\n\ndef test_external_function_exception_caught_by_try_except():\n    \"\"\"Test that exceptions from external functions can be caught by try/except.\"\"\"\n    code = \"\"\"\ntry:\n    fail()\nexcept ValueError:\n    caught = True\ncaught\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def fail(*args: Any, **kwargs: Any) -> None:\n        raise ValueError('caught error')\n\n    result = m.run(external_functions={'fail': fail})\n    assert result == snapshot(True)\n\n\ndef test_external_function_exception_type_preserved():\n    \"\"\"Test that various exception types are correctly preserved.\"\"\"\n    m = pydantic_monty.Monty('fail()')\n\n    def fail_type_error(*args: Any, **kwargs: Any) -> None:\n        raise TypeError('type error message')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'fail': fail_type_error})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, TypeError)\n    assert inner.args[0] == snapshot('type error message')\n\n\n@pytest.mark.parametrize(\n    'exception_class,exception_name',\n    [\n        # ArithmeticError hierarchy\n        (ZeroDivisionError, 'ZeroDivisionError'),\n        (OverflowError, 'OverflowError'),\n        (ArithmeticError, 'ArithmeticError'),\n        # RuntimeError hierarchy\n        (NotImplementedError, 'NotImplementedError'),\n        (RecursionError, 'RecursionError'),\n        (RuntimeError, 'RuntimeError'),\n        # LookupError hierarchy\n        (KeyError, 'KeyError'),\n        (IndexError, 'IndexError'),\n        (LookupError, 'LookupError'),\n        # Other exceptions\n        (ValueError, 'ValueError'),\n        (TypeError, 'TypeError'),\n        (AttributeError, 'AttributeError'),\n        (NameError, 'NameError'),\n        (AssertionError, 'AssertionError'),\n    ],\n)\ndef test_external_function_exception_hierarchy(exception_class: type[BaseException], exception_name: str):\n    \"\"\"Test that exception types in hierarchies are correctly preserved.\"\"\"\n    # Test that exception propagates with correct type\n    m = pydantic_monty.Monty('fail()')\n\n    def fail(*args: Any, **kwargs: Any) -> None:\n        raise exception_class('test message')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'fail': fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, exception_class)\n\n\n@pytest.mark.parametrize(\n    'exception_class,parent_class,expected_result',\n    [\n        # ArithmeticError hierarchy\n        (ZeroDivisionError, ArithmeticError, 'child'),\n        (OverflowError, ArithmeticError, 'child'),\n        # RuntimeError hierarchy\n        (NotImplementedError, RuntimeError, 'child'),\n        (RecursionError, RuntimeError, 'child'),\n        # LookupError hierarchy\n        (KeyError, LookupError, 'child'),\n        (IndexError, LookupError, 'child'),\n    ],\n)\ndef test_external_function_exception_caught_by_parent(\n    exception_class: type[BaseException], parent_class: type[BaseException], expected_result: str\n):\n    \"\"\"Test that child exceptions can be caught by parent except handlers.\"\"\"\n    code = f\"\"\"\ntry:\n    fail()\nexcept {parent_class.__name__}:\n    caught = 'parent'\nexcept {exception_class.__name__}:\n    caught = 'child'\ncaught\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def fail(*args: Any, **kwargs: Any) -> None:\n        raise exception_class('test')\n\n    # Child exception should be caught by parent handler (which comes first)\n    result = m.run(external_functions={'fail': fail})\n    assert result == 'parent'\n\n\n@pytest.mark.parametrize(\n    'exception_class,expected_result',\n    [\n        (ZeroDivisionError, 'ZeroDivisionError'),\n        (OverflowError, 'OverflowError'),\n        (NotImplementedError, 'NotImplementedError'),\n        (RecursionError, 'RecursionError'),\n        (KeyError, 'KeyError'),\n        (IndexError, 'IndexError'),\n    ],\n)\ndef test_external_function_exception_caught_specifically(exception_class: type[BaseException], expected_result: str):\n    \"\"\"Test that child exceptions can be caught by their specific handler.\"\"\"\n    code = f\"\"\"\ntry:\n    fail()\nexcept {exception_class.__name__}:\n    caught = '{expected_result}'\ncaught\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def fail(*args: Any, **kwargs: Any) -> None:\n        raise exception_class('test')\n\n    result = m.run(external_functions={'fail': fail})\n    assert result == expected_result\n\n\ndef test_external_function_exception_in_expression():\n    \"\"\"Test exception from external function in an expression context.\"\"\"\n    m = pydantic_monty.Monty('1 + fail() + 2')\n\n    def fail(*args: Any, **kwargs: Any) -> int:\n        raise RuntimeError('mid-expression error')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'fail': fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, RuntimeError)\n    assert inner.args[0] == snapshot('mid-expression error')\n\n\ndef test_external_function_exception_after_successful_call():\n    \"\"\"Test exception handling after a successful external call.\"\"\"\n    code = \"\"\"\na = success()\nb = fail()\na + b\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def success(*args: Any, **kwargs: Any) -> int:\n        return 10\n\n    def fail(*args: Any, **kwargs: Any) -> int:\n        raise ValueError('second call fails')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'success': success, 'fail': fail})\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('second call fails')\n\n\ndef test_external_function_exception_with_finally():\n    \"\"\"Test that finally block runs when external function raises.\"\"\"\n    code = \"\"\"\nfinally_ran = False\ntry:\n    fail()\nexcept ValueError:\n    pass\nfinally:\n    finally_ran = True\nfinally_ran\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def fail(*args: Any, **kwargs: Any) -> None:\n        raise ValueError('error')\n\n    result = m.run(external_functions={'fail': fail})\n    assert result == snapshot(True)\n"
  },
  {
    "path": "crates/monty-python/tests/test_inputs.py",
    "content": "import pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_single_input():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': 42}) == snapshot(42)\n\n\ndef test_multiple_inputs():\n    m = pydantic_monty.Monty('x + y + z', inputs=['x', 'y', 'z'])\n    assert m.run(inputs={'x': 1, 'y': 2, 'z': 3}) == snapshot(6)\n\n\ndef test_input_used_in_expression():\n    m = pydantic_monty.Monty('x * 2 + y', inputs=['x', 'y'])\n    assert m.run(inputs={'x': 5, 'y': 3}) == snapshot(13)\n\n\ndef test_input_string():\n    m = pydantic_monty.Monty('greeting + \" \" + name', inputs=['greeting', 'name'])\n    assert m.run(inputs={'greeting': 'Hello', 'name': 'World'}) == snapshot('Hello World')\n\n\ndef test_input_list():\n    m = pydantic_monty.Monty('data[0] + data[1]', inputs=['data'])\n    assert m.run(inputs={'data': [10, 20]}) == snapshot(30)\n\n\ndef test_input_dict():\n    m = pydantic_monty.Monty('config[\"a\"] * config[\"b\"]', inputs=['config'])\n    assert m.run(inputs={'config': {'a': 3, 'b': 4}}) == snapshot(12)\n\n\ndef test_missing_input_raises():\n    m = pydantic_monty.Monty('x + y', inputs=['x', 'y'])\n    with pytest.raises(KeyError, match=\"Missing required input: 'y'\"):\n        m.run(inputs={'x': 1})\n\n\ndef test_all_inputs_missing_raises():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    with pytest.raises(TypeError, match='Missing required inputs'):\n        m.run()\n\n\ndef test_no_inputs_declared_but_provided_raises():\n    m = pydantic_monty.Monty('1 + 1')\n    with pytest.raises(TypeError, match='No input variables declared but inputs dict was provided'):\n        m.run(inputs={'x': 1})\n        with pytest.raises(TypeError, match='No input variables declared but inputs dict was provided'):\n            m.run(inputs={})\n\n\ndef test_inputs_order_independent():\n    m = pydantic_monty.Monty('a - b', inputs=['a', 'b'])\n    # Dict order shouldn't matter\n    assert m.run(inputs={'b': 3, 'a': 10}) == snapshot(7)\n\n\ndef test_function_param_shadows_input():\n    \"\"\"Function parameter should shadow script input with the same name.\"\"\"\n    code = \"\"\"\ndef foo(x):\n    return x + 1\n\nfoo(x * 2)\n\"\"\"\n    m = pydantic_monty.Monty(code, inputs=['x'])\n    # x=5, so foo(x * 2) = foo(10), and inside foo, x is 10 (not 5), so returns 11\n    assert m.run(inputs={'x': 5}) == snapshot(11)\n\n\ndef test_function_param_shadows_input_multiple_params():\n    \"\"\"Multiple function parameters should all shadow their corresponding inputs.\"\"\"\n    code = \"\"\"\ndef add(x, y):\n    return x + y\n\nadd(x * 10, y * 100)\n\"\"\"\n    m = pydantic_monty.Monty(code, inputs=['x', 'y'])\n    # x=2, y=3, so add(20, 300) should return 320\n    assert m.run(inputs={'x': 2, 'y': 3}) == snapshot(320)\n\n\ndef test_input_accessible_outside_shadowing_function():\n    \"\"\"Script input should still be accessible outside the function that shadows it.\"\"\"\n    code = \"\"\"\ndef double(x):\n    return x * 2\n\nresult = double(10) + x\nresult\n\"\"\"\n    m = pydantic_monty.Monty(code, inputs=['x'])\n    # double(10) = 20, x (input) = 5, so result = 25\n    assert m.run(inputs={'x': 5}) == snapshot(25)\n\n\ndef test_function_param_shadows_input_with_default():\n    \"\"\"Function parameter with default should shadow script input when called with arg.\"\"\"\n    code = \"\"\"\ndef foo(x=100):\n    return x + 1\n\nfoo(x * 2)\n\"\"\"\n    m = pydantic_monty.Monty(code, inputs=['x'])\n    # x=5, foo(10), inside foo x=10 (not 5 or 100), returns 11\n    assert m.run(inputs={'x': 5}) == snapshot(11)\n\n\ndef test_function_uses_input_directly():\n    \"\"\"Function that doesn't shadow should still access the input.\"\"\"\n    code = \"\"\"\ndef foo(y):\n    return x + y\n\nfoo(10)\n\"\"\"\n    m = pydantic_monty.Monty(code, inputs=['x'])\n    # x=5 (input), foo(10) with y=10, returns x + y = 5 + 10 = 15\n    assert m.run(inputs={'x': 5}) == snapshot(15)\n"
  },
  {
    "path": "crates/monty-python/tests/test_limits.py",
    "content": "import multiprocessing\nimport os\nimport signal\nimport threading\nimport time\nfrom types import FrameType\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_resource_limits_custom():\n    limits = pydantic_monty.ResourceLimits(\n        max_allocations=100,\n        max_duration_secs=5.0,\n        max_memory=1024,\n        gc_interval=10,\n        max_recursion_depth=500,\n    )\n    assert limits.get('max_allocations') == snapshot(100)\n    assert limits.get('max_duration_secs') == snapshot(5.0)\n    assert limits.get('max_memory') == snapshot(1024)\n    assert limits.get('gc_interval') == snapshot(10)\n    assert limits.get('max_recursion_depth') == snapshot(500)\n\n\ndef test_resource_limits_repr():\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=1.0)\n    assert repr(limits) == snapshot(\"{'max_duration_secs': 1.0}\")\n\n\ndef test_run_with_limits():\n    m = pydantic_monty.Monty('1 + 1')\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=5.0)\n    assert m.run(limits=limits) == snapshot(2)\n\n\ndef test_recursion_limit():\n    code = \"\"\"\ndef recurse(n):\n    if n <= 0:\n        return 0\n    return 1 + recurse(n - 1)\n\nrecurse(10)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    limits = pydantic_monty.ResourceLimits(max_recursion_depth=5)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    assert isinstance(exc_info.value.exception(), RecursionError)\n\n\ndef test_recursion_limit_ok():\n    code = \"\"\"\ndef recurse(n):\n    if n <= 0:\n        return 0\n    return 1 + recurse(n - 1)\n\nrecurse(5)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    limits = pydantic_monty.ResourceLimits(max_recursion_depth=100)\n    assert m.run(limits=limits) == snapshot(5)\n\n\ndef test_allocation_limit():\n    # Note: allocation counting may not trigger on all operations\n    # Use a more aggressive allocation pattern\n    code = \"\"\"\nresult = []\nfor i in range(10000):\n    result.append([i])  # Each append creates a new list\nlen(result)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    limits = pydantic_monty.ResourceLimits(max_allocations=5)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    assert isinstance(exc_info.value.exception(), MemoryError)\n\n\ndef test_memory_limit():\n    code = \"\"\"\nresult = []\nfor i in range(1000):\n    result.append('x' * 100)\nlen(result)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    limits = pydantic_monty.ResourceLimits(max_memory=100)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    assert isinstance(exc_info.value.exception(), MemoryError)\n\n\ndef test_limits_with_inputs():\n    m = pydantic_monty.Monty('x * 2', inputs=['x'])\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=5.0)\n    assert m.run(inputs={'x': 21}, limits=limits) == snapshot(42)\n\n\ndef test_limits_wrong_type_raises_error():\n    m = pydantic_monty.Monty('1 + 1')\n    with pytest.raises(TypeError):\n        m.run(limits={'max_allocations': 'not an int'})  # pyright: ignore[reportArgumentType]\n\n\ndef test_limits_none_value_allowed():\n    m = pydantic_monty.Monty('1 + 1')\n    # None is valid to explicitly disable a limit\n    assert m.run(limits={'max_allocations': None}) == snapshot(2)  # pyright: ignore[reportArgumentType]\n\n\ndef test_signal_alarm_custom_error():\n    \"\"\"Test that custom signal handlers work during execution.\n\n    The idea here is we run another thread which sends a signal to the current process after a delay\n    then set up a signal handler to catch that signal and raise a custom exception.\n\n    So while monty is running, we have to run the code to catch the signal, and propagate that exception.\n    \"\"\"\n    code = \"\"\"\ndef fib(n):\n    if n <= 1:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nfib(35)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    def send_signal():\n        time.sleep(0.1)\n        os.kill(os.getpid(), signal.SIGINT)\n\n    def raise_potato(signum: int, frame: FrameType | None) -> None:\n        raise ValueError('potato')\n\n    thread = threading.Thread(target=send_signal)\n    thread.start()\n    old_handler = signal.signal(signal.SIGINT, raise_potato)\n    try:\n        with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n            m.run()\n        inner = exc_info.value.exception()\n        assert isinstance(inner, ValueError)\n        assert inner.args[0] == snapshot('potato')\n    finally:\n        thread.join()\n        signal.signal(signal.SIGINT, old_handler)\n\n\ndef _send_sigint_after_delay(pid: int, delay: float) -> None:\n    \"\"\"Helper function to send SIGINT to a process after a delay.\"\"\"\n    time.sleep(delay)\n    os.kill(pid, signal.SIGINT)\n\n\ndef test_keyboard_interrupt():\n    \"\"\"Test that KeyboardInterrupt is raised when SIGINT is sent during execution.\"\"\"\n    code = \"\"\"\ndef fib(n):\n    if n <= 1:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nfib(35)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    # Send SIGINT after a short delay using a separate process\n    proc = multiprocessing.Process(target=_send_sigint_after_delay, args=(os.getpid(), 0.05))\n    proc.start()\n    try:\n        raised_keyboard_interrupt = False\n        try:\n            m.run()\n        except pydantic_monty.MontyRuntimeError as e:\n            if isinstance(e.exception(), KeyboardInterrupt):\n                raised_keyboard_interrupt = True\n\n        assert raised_keyboard_interrupt, 'Expected KeyboardInterrupt to be raised'\n    finally:\n        proc.join()\n\n\ndef test_pow_memory_limit():\n    \"\"\"Large pow should fail when memory limit is set.\"\"\"\n    m = pydantic_monty.Monty('2 ** 10000000')\n    limits = pydantic_monty.ResourceLimits(max_memory=1_000_000)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    assert isinstance(exc_info.value.exception(), MemoryError)\n\n\ndef test_lshift_memory_limit():\n    \"\"\"Large left shift should fail when memory limit is set.\"\"\"\n    m = pydantic_monty.Monty('1 << 10000000')\n    limits = pydantic_monty.ResourceLimits(max_memory=1_000_000)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    assert isinstance(exc_info.value.exception(), MemoryError)\n\n\ndef test_mult_memory_limit():\n    \"\"\"Large multiplication should fail when memory limit is set.\"\"\"\n    # First create a large number, then try to square it\n    code = \"\"\"\nbig = 2 ** 4000000\nresult = big * big\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    limits = pydantic_monty.ResourceLimits(max_memory=1_000_000)\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    assert isinstance(exc_info.value.exception(), MemoryError)\n\n\ndef test_small_operations_within_limit():\n    \"\"\"Smaller operations should succeed even with limits.\"\"\"\n    m = pydantic_monty.Monty('2 ** 1000')\n    limits = pydantic_monty.ResourceLimits(max_memory=1_000_000)\n    result = m.run(limits=limits)\n    assert result > 0\n\n\n@pytest.mark.parametrize(\n    'code',\n    [\n        'sum(range(10**18))',\n        'list(range(10**18))',\n        'sorted(range(10**18))',\n        'min(range(10**18))',\n        'max(range(10**18))',\n    ],\n    ids=['sum', 'list', 'sorted', 'min', 'max'],\n)\ndef test_timeout_enforced_in_builtin_loops(code: str):\n    \"\"\"Timeout must be enforced inside Rust-side builtin iteration loops.\n\n    Previously, builtins like sum(), sorted(), min(), max() ran Rust-side loops\n    entirely within a single bytecode instruction, bypassing the VM's\n    per-instruction timeout check.\n    \"\"\"\n    m = pydantic_monty.Monty(code)\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=0.1)\n    start = time.monotonic()\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(limits=limits)\n    elapsed = time.monotonic() - start\n    assert isinstance(exc_info.value.exception(), TimeoutError)\n    # Should terminate promptly - well under 2 seconds\n    assert elapsed < 2.0\n"
  },
  {
    "path": "crates/monty-python/tests/test_os_access.py",
    "content": "\"\"\"Tests for OSAccess class functionality.\n\nThese tests verify the OSAccess class behavior - the high-level virtual filesystem\nthat can be passed to Monty.run(os=...). Most tests run Python code through Monty\nto verify behavior as it would be used in practice.\n\nFor tests of the AbstractOS interface via custom subclasses, see test_os_access_raw.py.\n\"\"\"\n\nfrom pathlib import PurePosixPath\nfrom typing import Any\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nfrom pydantic_monty import CallbackFile, MemoryFile, Monty, MontyRuntimeError, OSAccess\n\n# Alias for brevity in tests\nP = PurePosixPath\n\n# =============================================================================\n# OSAccess Initialization & Validation\n# =============================================================================\n\n\ndef test_non_absolute_path():\n    \"\"\"OSAccess rejects files with relative paths.\"\"\"\n    osa = OSAccess([MemoryFile('relative/path.txt', content='test')])\n    assert osa.files[0].path.as_posix() == '/relative/path.txt'\n\n    osa = OSAccess([MemoryFile('relative/path.txt', content='test')], root_dir='/foo/bar')\n    assert osa.files[0].path.as_posix() == '/foo/bar/relative/path.txt'\n\n\ndef test_file_nested_within_file_rejected():\n    \"\"\"OSAccess rejects files nested within another file's path.\"\"\"\n    with pytest.raises(ValueError) as exc_info:\n        OSAccess(\n            [\n                MemoryFile('/test/file.txt', content='outer'),\n                MemoryFile('/test/file.txt/nested.txt', content='inner'),\n            ]\n        )\n    assert str(exc_info.value) == snapshot(\n        \"Cannot put file MemoryFile(path=/test/file.txt/nested.txt, content='...', permissions=420) \"\n        \"within sub-directory of file MemoryFile(path=/test/file.txt, content='...', permissions=420)\"\n    )\n\n\ndef test_empty_initialization():\n    \"\"\"OSAccess can be initialized with no files.\"\"\"\n    fs = OSAccess()\n    result = Monty('from pathlib import Path; Path(\"/any/path\").exists()').run(os=fs)\n    assert result is False\n\n\ndef test_environ_parameter():\n    \"\"\"OSAccess accepts environ parameter for environment variables.\"\"\"\n    fs = OSAccess(environ={'MY_VAR': 'my_value'})\n    result = Monty(\"import os; os.getenv('MY_VAR')\").run(os=fs)\n    assert result == snapshot('my_value')\n\n\n# =============================================================================\n# Path Existence Checks (via Monty)\n# =============================================================================\n\n\ndef test_path_exists_file():\n    \"\"\"path_exists returns True for existing files.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").exists()').run(os=fs)\n    assert result is True\n\n\ndef test_path_exists_directory():\n    \"\"\"path_exists returns True for directories created by file paths.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/subdir\").exists()').run(os=fs)\n    assert result is True\n\n\ndef test_path_exists_nested():\n    \"\"\"path_exists handles deeply nested paths.\"\"\"\n    fs = OSAccess([MemoryFile('/a/b/c/d/file.txt', content='deep')])\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/a').exists(), Path('/a/b').exists(), Path('/a/b/c').exists(), Path('/a/b/c/d').exists())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot((True, True, True, True))\n\n\ndef test_path_exists_missing():\n    \"\"\"path_exists returns False for non-existent paths.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/other/path\").exists()').run(os=fs)\n    assert result is False\n\n\ndef test_path_is_file_for_file():\n    \"\"\"path_is_file returns True for files.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").is_file()').run(os=fs)\n    assert result is True\n\n\ndef test_path_is_file_for_directory():\n    \"\"\"path_is_file returns False for directories.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/subdir\").is_file()').run(os=fs)\n    assert result is False\n\n\ndef test_path_is_file_missing():\n    \"\"\"path_is_file returns False for non-existent paths.\"\"\"\n    fs = OSAccess()\n    result = Monty('from pathlib import Path; Path(\"/missing\").is_file()').run(os=fs)\n    assert result is False\n\n\ndef test_path_is_dir_for_directory():\n    \"\"\"path_is_dir returns True for directories.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/subdir\").is_dir()').run(os=fs)\n    assert result is True\n\n\ndef test_path_is_dir_for_file():\n    \"\"\"path_is_dir returns False for files.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").is_dir()').run(os=fs)\n    assert result is False\n\n\ndef test_path_is_dir_missing():\n    \"\"\"path_is_dir returns False for non-existent paths.\"\"\"\n    fs = OSAccess()\n    result = Monty('from pathlib import Path; Path(\"/missing\").is_dir()').run(os=fs)\n    assert result is False\n\n\ndef test_path_is_symlink_always_false():\n    \"\"\"path_is_symlink always returns False (no symlink support).\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/test/file.txt').is_symlink(), Path('/test').is_symlink(), Path('/missing').is_symlink())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot((False, False, False))\n\n\n# =============================================================================\n# Reading Files (via Monty)\n# =============================================================================\n\n\ndef test_read_text_string_content():\n    \"\"\"path_read_text returns string content directly.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello world')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").read_text()').run(os=fs)\n    assert result == snapshot('hello world')\n\n\ndef test_read_text_bytes_content_decoded():\n    \"\"\"path_read_text decodes bytes content as UTF-8.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content=b'bytes content')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").read_text()').run(os=fs)\n    assert result == snapshot('bytes content')\n\n\ndef test_read_bytes_bytes_content():\n    \"\"\"path_read_bytes returns bytes content directly.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.bin', content=b'\\x00\\x01\\x02\\x03')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.bin\").read_bytes()').run(os=fs)\n    assert result == snapshot(b'\\x00\\x01\\x02\\x03')\n\n\ndef test_read_bytes_string_content_encoded():\n    \"\"\"path_read_bytes encodes string content as UTF-8.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").read_bytes()').run(os=fs)\n    assert result == snapshot(b'hello')\n\n\ndef test_read_text_file_not_found():\n    \"\"\"path_read_text raises FileNotFoundError for missing files.\"\"\"\n    fs = OSAccess()\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty('from pathlib import Path; Path(\"/missing.txt\").read_text()').run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing.txt'\")\n\n\ndef test_read_bytes_file_not_found():\n    \"\"\"path_read_bytes raises FileNotFoundError for missing files.\"\"\"\n    fs = OSAccess()\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty('from pathlib import Path; Path(\"/missing.bin\").read_bytes()').run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing.bin'\")\n\n\ndef test_read_text_is_a_directory():\n    \"\"\"path_read_text raises error for directories.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty('from pathlib import Path; Path(\"/test/subdir\").read_text()').run(os=fs)\n    # Monty reports this as OSError, not IsADirectoryError\n    assert str(exc_info.value) == snapshot(\"IsADirectoryError: [Errno 21] Is a directory: '/test/subdir'\")\n\n\ndef test_read_bytes_is_a_directory():\n    \"\"\"path_read_bytes raises error for directories.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty('from pathlib import Path; Path(\"/test/subdir\").read_bytes()').run(os=fs)\n    # Monty reports this as OSError, not IsADirectoryError\n    assert str(exc_info.value) == snapshot(\"IsADirectoryError: [Errno 21] Is a directory: '/test/subdir'\")\n\n\n# =============================================================================\n# Writing Files (via Monty)\n# =============================================================================\n\n\ndef test_write_text_via_monty():\n    \"\"\"Path.write_text() creates a new file via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/existing.txt', content='existing')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/new.txt').write_text('new content')\n\"\"\"\n    result = Monty(code).run(os=fs)\n    # write_text returns the number of bytes written\n    assert result == snapshot(11)\n\n    # Verify file was created\n    assert fs.path_exists(P('/test/new.txt')) is True\n    assert fs.path_read_text(P('/test/new.txt')) == 'new content'\n\n\ndef test_write_text_overwrite_via_monty():\n    \"\"\"Path.write_text() overwrites existing file via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='original')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/file.txt').write_text('updated')\n\"\"\"\n    Monty(code).run(os=fs)\n    assert fs.path_read_text(P('/test/file.txt')) == 'updated'\n\n\ndef test_write_bytes_via_monty():\n    \"\"\"Path.write_bytes() creates a new file via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/existing.txt', content='existing')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/new.bin').write_bytes(b'binary data')\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(11)\n    assert fs.path_read_bytes(P('/test/new.bin')) == b'binary data'\n\n\ndef test_write_text_parent_not_exists_via_monty():\n    \"\"\"Path.write_text() raises FileNotFoundError when parent doesn't exist via Monty.\"\"\"\n    fs = OSAccess()\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/no/parent/file.txt').write_text('test')\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\n        \"FileNotFoundError: [Errno 2] No such file or directory: '/no/parent/file.txt'\"\n    )\n\n\ndef test_write_text_to_directory_via_monty():\n    \"\"\"Path.write_text() raises IsADirectoryError when writing to a directory via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/test/subdir').write_text('test')\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"IsADirectoryError: [Errno 21] Is a directory: '/test/subdir'\")\n\n\n# =============================================================================\n# Writing Files (via direct API)\n# =============================================================================\n\n\ndef test_write_text_new_file_direct():\n    \"\"\"path_write_text creates a new file via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/existing.txt', content='existing')])\n\n    # Write a new file\n    fs.path_write_text(P('/test/new.txt'), 'new content')\n\n    # Verify it was created\n    assert fs.path_exists(P('/test/new.txt')) is True\n    assert fs.path_read_text(P('/test/new.txt')) == 'new content'\n\n\ndef test_write_text_overwrite_existing_direct():\n    \"\"\"path_write_text overwrites existing file content via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='original')])\n\n    fs.path_write_text(P('/test/file.txt'), 'updated')\n    assert fs.path_read_text(P('/test/file.txt')) == 'updated'\n\n\ndef test_write_bytes_new_file_direct():\n    \"\"\"path_write_bytes creates a new file via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/existing.txt', content='existing')])\n\n    fs.path_write_bytes(P('/test/new.bin'), b'binary data')\n    assert fs.path_read_bytes(P('/test/new.bin')) == b'binary data'\n\n\ndef test_write_bytes_overwrite_existing_direct():\n    \"\"\"path_write_bytes overwrites existing file content via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.bin', content=b'original')])\n\n    fs.path_write_bytes(P('/test/file.bin'), b'updated')\n    assert fs.path_read_bytes(P('/test/file.bin')) == b'updated'\n\n\ndef test_write_text_parent_not_exists_direct():\n    \"\"\"path_write_text raises FileNotFoundError when parent doesn't exist via direct API.\"\"\"\n    fs = OSAccess()\n    with pytest.raises(FileNotFoundError) as exc_info:\n        fs.path_write_text(P('/no/parent/file.txt'), 'test')\n    assert str(exc_info.value) == snapshot(\"[Errno 2] No such file or directory: '/no/parent/file.txt'\")\n\n\ndef test_write_text_to_directory_direct():\n    \"\"\"path_write_text raises IsADirectoryError when writing to a directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    with pytest.raises(IsADirectoryError) as exc_info:\n        fs.path_write_text(P('/test/subdir'), 'test')\n    assert str(exc_info.value) == snapshot(\"[Errno 21] Is a directory: '/test/subdir'\")\n\n\n# =============================================================================\n# Directory Operations - mkdir (via Monty)\n# =============================================================================\n\n\ndef test_mkdir_basic_via_monty():\n    \"\"\"Path.mkdir() creates a directory via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/newdir').mkdir()\n\"\"\"\n    Monty(code).run(os=fs)\n    assert fs.path_is_dir(P('/test/newdir')) is True\n\n\ndef test_mkdir_with_parents_via_monty():\n    \"\"\"Path.mkdir(parents=True) creates parent directories via Monty.\"\"\"\n    fs = OSAccess()\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/a/b/c/d').mkdir(parents=True)\n\"\"\"\n    Monty(code).run(os=fs)\n    assert fs.path_is_dir(P('/a')) is True\n    assert fs.path_is_dir(P('/a/b')) is True\n    assert fs.path_is_dir(P('/a/b/c')) is True\n    assert fs.path_is_dir(P('/a/b/c/d')) is True\n\n\ndef test_mkdir_exist_ok_true_via_monty():\n    \"\"\"Path.mkdir(exist_ok=True) doesn't raise for existing directory via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/subdir').mkdir(exist_ok=True)\n\"\"\"\n    # Should not raise\n    Monty(code).run(os=fs)\n    assert fs.path_is_dir(P('/test/subdir')) is True\n\n\ndef test_mkdir_exist_ok_false_via_monty():\n    \"\"\"Path.mkdir() raises OSError (FileExistsError) for existing directory via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/test/subdir').mkdir()\").run(os=fs)\n    # Monty maps FileExistsError to OSError\n    assert str(exc_info.value) == snapshot(\"FileExistsError: [Errno 17] File exists: '/test/subdir'\")\n\n\ndef test_mkdir_parent_not_exists_via_monty():\n    \"\"\"Path.mkdir() raises FileNotFoundError when parent doesn't exist via Monty.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/no/parent/dir').mkdir()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/no/parent/dir'\")\n\n\n# =============================================================================\n# Directory Operations - mkdir (via direct API)\n# =============================================================================\n\n\ndef test_mkdir_basic_direct():\n    \"\"\"path_mkdir creates a directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    fs.path_mkdir(P('/test/newdir'), parents=False, exist_ok=False)\n    assert fs.path_is_dir(P('/test/newdir')) is True\n\n\ndef test_mkdir_with_parents_direct():\n    \"\"\"path_mkdir with parents=True creates parent directories via direct API.\"\"\"\n    fs = OSAccess()\n\n    fs.path_mkdir(P('/a/b/c/d'), parents=True, exist_ok=False)\n    assert fs.path_is_dir(P('/a')) is True\n    assert fs.path_is_dir(P('/a/b')) is True\n    assert fs.path_is_dir(P('/a/b/c')) is True\n    assert fs.path_is_dir(P('/a/b/c/d')) is True\n\n\ndef test_mkdir_exist_ok_true_direct():\n    \"\"\"path_mkdir with exist_ok=True doesn't raise for existing directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    # Should not raise\n    fs.path_mkdir(P('/test/subdir'), parents=False, exist_ok=True)\n    assert fs.path_is_dir(P('/test/subdir')) is True\n\n\ndef test_mkdir_exist_ok_false_direct():\n    \"\"\"path_mkdir with exist_ok=False raises for existing directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    with pytest.raises(FileExistsError) as exc_info:\n        fs.path_mkdir(P('/test/subdir'), parents=False, exist_ok=False)\n    assert str(exc_info.value) == snapshot(\"[Errno 17] File exists: '/test/subdir'\")\n\n\ndef test_mkdir_file_exists_direct():\n    \"\"\"path_mkdir raises FileExistsError when a file exists at the path via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    with pytest.raises(FileExistsError) as exc_info:\n        fs.path_mkdir(P('/test/file.txt'), parents=False, exist_ok=False)\n    assert str(exc_info.value) == snapshot(\"[Errno 17] File exists: '/test/file.txt'\")\n\n\ndef test_mkdir_parent_not_exists_direct():\n    \"\"\"path_mkdir without parents raises FileNotFoundError when parent doesn't exist via direct API.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(FileNotFoundError) as exc_info:\n        fs.path_mkdir(P('/no/parent/dir'), parents=False, exist_ok=False)\n    assert str(exc_info.value) == snapshot(\"[Errno 2] No such file or directory: '/no/parent/dir'\")\n\n\ndef test_mkdir_parent_is_file_direct():\n    \"\"\"path_mkdir raises NotADirectoryError when parent is a file via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    with pytest.raises(NotADirectoryError) as exc_info:\n        fs.path_mkdir(P('/test/file.txt/subdir'), parents=True, exist_ok=False)\n    assert str(exc_info.value) == snapshot(\"[Errno 20] Not a directory: '/test/file.txt/subdir'\")\n\n\n# =============================================================================\n# Directory Operations - rmdir (via Monty)\n# =============================================================================\n\n\ndef test_rmdir_empty_directory_via_monty():\n    \"\"\"Path.rmdir() removes an empty directory via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    fs.path_mkdir(P('/test/newdir'), parents=False, exist_ok=False)\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/newdir').rmdir()\n\"\"\"\n    Monty(code).run(os=fs)\n    assert fs.path_exists(P('/test/newdir')) is False\n\n\ndef test_rmdir_non_empty_directory_via_monty():\n    \"\"\"Path.rmdir() raises OSError for non-empty directory via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/test/subdir').rmdir()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"OSError: [Errno 39] Directory not empty: '/test/subdir'\")\n\n\ndef test_rmdir_not_found_via_monty():\n    \"\"\"Path.rmdir() raises FileNotFoundError for non-existent path via Monty.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/missing').rmdir()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing'\")\n\n\ndef test_rmdir_file_not_directory_via_monty():\n    \"\"\"Path.rmdir() raises NotADirectoryError for files via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/test/file.txt').rmdir()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"NotADirectoryError: [Errno 20] Not a directory: '/test/file.txt'\")\n\n\n# =============================================================================\n# Directory Operations - rmdir (via direct API)\n# =============================================================================\n\n\ndef test_rmdir_empty_directory_direct():\n    \"\"\"path_rmdir removes an empty directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    fs.path_mkdir(P('/test/newdir'), parents=False, exist_ok=False)\n    fs.path_rmdir(P('/test/newdir'))\n    assert fs.path_exists(P('/test/newdir')) is False\n\n\ndef test_rmdir_non_empty_directory_direct():\n    \"\"\"path_rmdir raises OSError for non-empty directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    with pytest.raises(OSError) as exc_info:\n        fs.path_rmdir(P('/test/subdir'))\n    assert str(exc_info.value) == snapshot(\"[Errno 39] Directory not empty: '/test/subdir'\")\n\n\ndef test_rmdir_file_not_directory_direct():\n    \"\"\"path_rmdir raises NotADirectoryError for files via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    with pytest.raises(NotADirectoryError) as exc_info:\n        fs.path_rmdir(P('/test/file.txt'))\n    assert str(exc_info.value) == snapshot(\"[Errno 20] Not a directory: '/test/file.txt'\")\n\n\ndef test_rmdir_not_found_direct():\n    \"\"\"path_rmdir raises FileNotFoundError for non-existent path via direct API.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(FileNotFoundError) as exc_info:\n        fs.path_rmdir(P('/missing'))\n    assert str(exc_info.value) == snapshot(\"[Errno 2] No such file or directory: '/missing'\")\n\n\n# =============================================================================\n# Directory Operations - iterdir (via Monty)\n# =============================================================================\n\n\ndef test_iterdir_list_contents():\n    \"\"\"path_iterdir lists directory contents.\"\"\"\n    fs = OSAccess(\n        [\n            MemoryFile('/test/a.txt', content='a'),\n            MemoryFile('/test/b.txt', content='b'),\n            MemoryFile('/test/subdir/c.txt', content='c'),\n        ]\n    )\n    code = \"\"\"\nfrom pathlib import Path\n[str(p) for p in Path('/test').iterdir()]\n\"\"\"\n    result = Monty(code).run(os=fs)\n    # Result may be in any order, so sort in Python\n    assert sorted(result) == snapshot(['/test/a.txt', '/test/b.txt', '/test/subdir'])\n\n\ndef test_iterdir_empty_directory_direct():\n    \"\"\"path_iterdir returns empty list for empty directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    fs.path_mkdir(P('/test/empty'), parents=False, exist_ok=False)\n\n    result = fs.path_iterdir(P('/test/empty'))\n    assert result == snapshot([])\n\n\ndef test_iterdir_not_a_directory_direct():\n    \"\"\"path_iterdir raises NotADirectoryError for files via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    with pytest.raises(NotADirectoryError) as exc_info:\n        fs.path_iterdir(P('/test/file.txt'))\n    assert str(exc_info.value) == snapshot(\"[Errno 20] Not a directory: '/test/file.txt'\")\n\n\ndef test_iterdir_not_found():\n    \"\"\"path_iterdir raises FileNotFoundError for non-existent path.\"\"\"\n    fs = OSAccess()\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; list(Path('/missing').iterdir())\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing'\")\n\n\n# =============================================================================\n# File Operations - unlink (via Monty)\n# =============================================================================\n\n\ndef test_unlink_file_via_monty():\n    \"\"\"Path.unlink() removes a file via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/file.txt').unlink()\n\"\"\"\n    Monty(code).run(os=fs)\n    assert fs.path_exists(P('/test/file.txt')) is False\n\n\ndef test_unlink_file_not_found_via_monty():\n    \"\"\"Path.unlink() raises FileNotFoundError for non-existent files via Monty.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/missing.txt').unlink()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing.txt'\")\n\n\ndef test_unlink_is_directory_via_monty():\n    \"\"\"Path.unlink() raises IsADirectoryError for directories via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/test/subdir').unlink()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"IsADirectoryError: [Errno 21] Is a directory: '/test/subdir'\")\n\n\n# =============================================================================\n# File Operations - unlink (via direct API)\n# =============================================================================\n\n\ndef test_unlink_file_direct():\n    \"\"\"path_unlink removes a file via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n\n    fs.path_unlink(P('/test/file.txt'))\n    assert fs.path_exists(P('/test/file.txt')) is False\n\n\ndef test_unlink_file_not_found_direct():\n    \"\"\"path_unlink raises FileNotFoundError for non-existent files via direct API.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(FileNotFoundError) as exc_info:\n        fs.path_unlink(P('/missing.txt'))\n    assert str(exc_info.value) == snapshot(\"[Errno 2] No such file or directory: '/missing.txt'\")\n\n\ndef test_unlink_is_directory_direct():\n    \"\"\"path_unlink raises IsADirectoryError for directories via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n\n    with pytest.raises(IsADirectoryError) as exc_info:\n        fs.path_unlink(P('/test/subdir'))\n    assert str(exc_info.value) == snapshot(\"[Errno 21] Is a directory: '/test/subdir'\")\n\n\n# =============================================================================\n# Stat Operations (via Monty)\n# =============================================================================\n\n\ndef test_stat_file():\n    \"\"\"path_stat returns stat result for files with size and mode.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello world')])\n    code = \"\"\"\nfrom pathlib import Path\ns = Path('/test/file.txt').stat()\n(s.st_size, s.st_mode & 0o777)\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot((11, 0o644))\n\n\ndef test_stat_file_custom_permissions():\n    \"\"\"path_stat returns custom file permissions.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello', permissions=0o755)])\n    code = \"\"\"\nfrom pathlib import Path\ns = Path('/test/file.txt').stat()\ns.st_mode & 0o777\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(0o755)\n\n\ndef test_stat_directory():\n    \"\"\"path_stat returns stat result for directories.\"\"\"\n    fs = OSAccess([MemoryFile('/test/subdir/file.txt', content='hello')])\n    code = \"\"\"\nfrom pathlib import Path\ns = Path('/test/subdir').stat()\ns.st_mode\n\"\"\"\n    result = Monty(code).run(os=fs)\n    # Directory mode bits: 0o040000 (directory) | 0o755 (default perms) = 0o040755\n    assert result == snapshot(0o040755)\n\n\ndef test_stat_file_not_found():\n    \"\"\"path_stat raises FileNotFoundError for non-existent paths.\"\"\"\n    fs = OSAccess()\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/missing').stat()\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\"FileNotFoundError: [Errno 2] No such file or directory: '/missing'\")\n\n\ndef test_stat_bytes_content_size():\n    \"\"\"path_stat calculates size correctly for bytes content.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.bin', content=b'\\x00\\x01\\x02\\x03\\x04')])\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/file.bin').stat().st_size\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(5)\n\n\ndef test_stat_unicode_size():\n    \"\"\"path_stat calculates size as encoded UTF-8 bytes for string content.\"\"\"\n    # Unicode snowman is 3 bytes in UTF-8\n    fs = OSAccess([MemoryFile('/test/file.txt', content='☃')])\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/file.txt').stat().st_size\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(3)\n\n\n# =============================================================================\n# Rename Operations (via Monty)\n# =============================================================================\n\n\ndef test_rename_file_via_monty():\n    \"\"\"Path.rename() renames a file via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/old.txt', content='content')])\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/test/old.txt').rename(Path('/test/new.txt'))\n\"\"\"\n    Monty(code).run(os=fs)\n\n    assert fs.path_exists(P('/test/old.txt')) is False\n    assert fs.path_exists(P('/test/new.txt')) is True\n    assert fs.path_read_text(P('/test/new.txt')) == 'content'\n\n\ndef test_rename_source_not_found_via_monty():\n    \"\"\"Path.rename() raises FileNotFoundError when source doesn't exist via Monty.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/missing.txt').rename(Path('/new.txt'))\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\n        \"FileNotFoundError: [Errno 2] No such file or directory: '/missing.txt' -> '/new.txt'\"\n    )\n\n\ndef test_rename_target_parent_not_found_via_monty():\n    \"\"\"Path.rename() raises FileNotFoundError when target parent doesn't exist via Monty.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='content')])\n\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"from pathlib import Path; Path('/test/file.txt').rename(Path('/no/parent/file.txt'))\").run(os=fs)\n    assert str(exc_info.value) == snapshot(\n        \"FileNotFoundError: [Errno 2] No such file or directory: '/test/file.txt' -> '/no/parent/file.txt'\"\n    )\n\n\n# =============================================================================\n# Rename Operations (via direct API)\n# =============================================================================\n\n\ndef test_rename_file_direct():\n    \"\"\"path_rename renames a file via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/old.txt', content='content')])\n\n    fs.path_rename(P('/test/old.txt'), P('/test/new.txt'))\n\n    assert fs.path_exists(P('/test/old.txt')) is False\n    assert fs.path_exists(P('/test/new.txt')) is True\n    assert fs.path_read_text(P('/test/new.txt')) == 'content'\n\n\ndef test_rename_source_not_found_direct():\n    \"\"\"path_rename raises FileNotFoundError when source doesn't exist via direct API.\"\"\"\n    fs = OSAccess()\n\n    with pytest.raises(FileNotFoundError) as exc_info:\n        fs.path_rename(P('/missing.txt'), P('/new.txt'))\n    assert str(exc_info.value) == snapshot(\"[Errno 2] No such file or directory: '/missing.txt' -> '/new.txt'\")\n\n\ndef test_rename_target_parent_not_found_direct():\n    \"\"\"path_rename raises FileNotFoundError when target parent doesn't exist via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='content')])\n\n    with pytest.raises(FileNotFoundError) as exc_info:\n        fs.path_rename(P('/test/file.txt'), P('/no/parent/file.txt'))\n    assert str(exc_info.value) == snapshot(\n        \"[Errno 2] No such file or directory: '/test/file.txt' -> '/no/parent/file.txt'\"\n    )\n\n\ndef test_rename_directory_direct():\n    \"\"\"path_rename renames a directory via direct API.\"\"\"\n    fs = OSAccess([MemoryFile('/test/olddir/file.txt', content='content')])\n    fs.path_mkdir(P('/test/newdir'), parents=False, exist_ok=False)\n\n    fs.path_rename(P('/test/newdir'), P('/test/renamed'))\n    assert fs.path_is_dir(P('/test/renamed')) is True\n\n\ndef test_rename_directory_non_empty_target_direct():\n    \"\"\"path_rename raises OSError when renaming directory to non-empty target via direct API.\"\"\"\n    fs = OSAccess(\n        [\n            MemoryFile('/test/src/a.txt', content='a'),\n            MemoryFile('/test/dst/b.txt', content='b'),\n        ]\n    )\n\n    with pytest.raises(OSError) as exc_info:\n        fs.path_rename(P('/test/src'), P('/test/dst'))\n    assert str(exc_info.value) == snapshot(\"[Errno 66] Directory not empty: '/test/src' -> '/test/dst'\")\n\n\ndef test_rename_directory_updates_file_paths_direct():\n    \"\"\"path_rename updates paths of all files within renamed directory.\"\"\"\n    file1 = MemoryFile('/old/dir/file1.txt', content='one')\n    file2 = MemoryFile('/old/dir/subdir/file2.txt', content='two')\n    fs = OSAccess([file1, file2])\n\n    # Create target parent and rename the directory\n    fs.path_mkdir(P('/new'), parents=False, exist_ok=False)\n    fs.path_rename(P('/old/dir'), P('/new/location'))\n\n    # Verify files are accessible at new paths\n    assert fs.path_read_text(P('/new/location/file1.txt')) == 'one'\n    assert fs.path_read_text(P('/new/location/subdir/file2.txt')) == 'two'\n\n    # Verify the AbstractFile objects have updated paths\n    assert file1.path.as_posix() == '/new/location/file1.txt'\n    assert file2.path.as_posix() == '/new/location/subdir/file2.txt'\n\n    # Verify old paths no longer exist\n    assert fs.path_exists(P('/old/dir')) is False\n    assert fs.path_exists(P('/old/dir/file1.txt')) is False\n\n\n# =============================================================================\n# Path Resolution (via Monty)\n# =============================================================================\n\n\ndef test_path_resolve_absolute():\n    \"\"\"path_resolve returns absolute path.\"\"\"\n    fs = OSAccess([MemoryFile('/test/file.txt', content='hello')])\n    code = \"\"\"\nfrom pathlib import Path\nstr(Path('/test/file.txt').resolve())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot('/test/file.txt')\n\n\ndef test_path_absolute_already_absolute():\n    \"\"\"path_absolute returns same path for already absolute path.\"\"\"\n    fs = OSAccess()\n    code = \"\"\"\nfrom pathlib import Path\nstr(Path('/already/absolute').absolute())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot('/already/absolute')\n\n\ndef test_path_absolute_relative():\n    \"\"\"path_absolute converts relative path to absolute.\"\"\"\n    fs = OSAccess()\n    code = \"\"\"\nfrom pathlib import Path\nstr(Path('relative/path').absolute())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot('/relative/path')\n\n\ndef test_path_resolve_same_as_absolute():\n    \"\"\"path_resolve behaves same as absolute (no symlinks in OSAccess).\"\"\"\n    fs = OSAccess()\n    code = \"\"\"\nfrom pathlib import Path\nstr(Path('relative').resolve()) == str(Path('relative').absolute())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result is True\n\n\n# =============================================================================\n# Environment Variables (via Monty)\n# =============================================================================\n\n\ndef test_getenv_existing_key():\n    \"\"\"getenv returns value for existing key.\"\"\"\n    fs = OSAccess(environ={'MY_VAR': 'my_value'})\n    result = Monty(\"import os; os.getenv('MY_VAR')\").run(os=fs)\n    assert result == snapshot('my_value')\n\n\ndef test_getenv_missing_key():\n    \"\"\"getenv returns None for missing key.\"\"\"\n    fs = OSAccess(environ={'OTHER': 'value'})\n    result = Monty(\"import os; os.getenv('MISSING')\").run(os=fs)\n    assert result is None\n\n\ndef test_getenv_missing_with_default():\n    \"\"\"getenv returns default for missing key when default provided.\"\"\"\n    fs = OSAccess(environ={})\n    result = Monty(\"import os; os.getenv('MISSING', 'default_value')\").run(os=fs)\n    assert result == snapshot('default_value')\n\n\ndef test_getenv_multiple_vars():\n    \"\"\"getenv handles multiple environment variables.\"\"\"\n    fs = OSAccess(environ={'VAR1': 'value1', 'VAR2': 'value2', 'VAR3': 'value3'})\n    code = \"\"\"\nimport os\n(os.getenv('VAR1'), os.getenv('VAR2'), os.getenv('VAR3'))\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(('value1', 'value2', 'value3'))\n\n\ndef test_get_environ_returns_dict():\n    \"\"\"os.environ returns the full environ dict.\"\"\"\n    fs = OSAccess(environ={'HOME': '/home/user', 'USER': 'testuser'})\n    result = Monty('import os; os.environ').run(os=fs)\n    assert result == snapshot({'HOME': '/home/user', 'USER': 'testuser'})\n\n\ndef test_get_environ_key_access():\n    \"\"\"os.environ['KEY'] returns the value.\"\"\"\n    fs = OSAccess(environ={'MY_VAR': 'my_value'})\n    result = Monty(\"import os; os.environ['MY_VAR']\").run(os=fs)\n    assert result == snapshot('my_value')\n\n\ndef test_get_environ_key_missing_raises():\n    \"\"\"os.environ['MISSING'] raises KeyError.\"\"\"\n    fs = OSAccess(environ={})\n    with pytest.raises(MontyRuntimeError) as exc_info:\n        Monty(\"import os; os.environ['MISSING']\").run(os=fs)\n    assert str(exc_info.value) == snapshot('KeyError: MISSING')\n\n\ndef test_get_environ_get_method():\n    \"\"\"os.environ.get() works correctly.\"\"\"\n    fs = OSAccess(environ={'HOME': '/home/user'})\n    result = Monty(\"import os; os.environ.get('HOME')\").run(os=fs)\n    assert result == snapshot('/home/user')\n\n\ndef test_get_environ_get_missing_with_default():\n    \"\"\"os.environ.get() returns default for missing key.\"\"\"\n    fs = OSAccess(environ={})\n    result = Monty(\"import os; os.environ.get('MISSING', 'fallback')\").run(os=fs)\n    assert result == snapshot('fallback')\n\n\ndef test_get_environ_len():\n    \"\"\"len(os.environ) returns the number of env vars.\"\"\"\n    fs = OSAccess(environ={'A': '1', 'B': '2', 'C': '3'})\n    result = Monty('import os; len(os.environ)').run(os=fs)\n    assert result == snapshot(3)\n\n\ndef test_get_environ_contains():\n    \"\"\"'KEY' in os.environ tests membership.\"\"\"\n    fs = OSAccess(environ={'PRESENT': 'value'})\n    code = \"\"\"\nimport os\n('PRESENT' in os.environ, 'ABSENT' in os.environ)\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot((True, False))\n\n\ndef test_get_environ_keys():\n    \"\"\"os.environ.keys() returns the keys.\"\"\"\n    fs = OSAccess(environ={'X': '1', 'Y': '2'})\n    result = Monty('import os; list(os.environ.keys())').run(os=fs)\n    assert set(result) == snapshot({'X', 'Y'})\n\n\ndef test_get_environ_values():\n    \"\"\"os.environ.values() returns the values.\"\"\"\n    fs = OSAccess(environ={'X': 'a', 'Y': 'b'})\n    result = Monty('import os; list(os.environ.values())').run(os=fs)\n    assert set(result) == snapshot({'a', 'b'})\n\n\ndef test_get_environ_items():\n    \"\"\"os.environ.items() returns key-value pairs.\"\"\"\n    fs = OSAccess(environ={'X': '1', 'Y': '2'})\n    result = Monty('import os; list(os.environ.items())').run(os=fs)\n    assert set(result) == snapshot({('X', '1'), ('Y', '2')})\n\n\ndef test_get_environ_empty():\n    \"\"\"os.environ returns empty dict when no environ provided.\"\"\"\n    fs = OSAccess()\n    result = Monty('import os; os.environ').run(os=fs)\n    assert result == snapshot({})\n\n\n# =============================================================================\n# MemoryFile Behavior\n# =============================================================================\n\n\ndef test_memory_file_string_content():\n    \"\"\"MemoryFile stores and returns string content.\"\"\"\n    file = MemoryFile('/test/file.txt', content='hello')\n    assert file.read_content() == snapshot('hello')\n    assert file.path.as_posix() == snapshot('/test/file.txt')\n    assert file.name == snapshot('file.txt')\n\n\ndef test_memory_file_bytes_content():\n    \"\"\"MemoryFile stores and returns bytes content.\"\"\"\n    file = MemoryFile('/test/file.bin', content=b'\\x00\\x01\\x02')\n    assert file.read_content() == snapshot(b'\\x00\\x01\\x02')\n\n\ndef test_memory_file_custom_permissions():\n    \"\"\"MemoryFile accepts custom permissions.\"\"\"\n    file = MemoryFile('/test/exec.sh', content='#!/bin/bash', permissions=0o755)\n    assert file.permissions == snapshot(0o755)\n\n\ndef test_memory_file_write_and_read():\n    \"\"\"MemoryFile supports writing and re-reading content.\"\"\"\n    file = MemoryFile('/test/file.txt', content='original')\n    file.write_content('updated')\n    assert file.read_content() == snapshot('updated')\n\n\ndef test_memory_file_delete():\n    \"\"\"MemoryFile can be marked as deleted.\"\"\"\n    file = MemoryFile('/test/file.txt', content='content')\n    assert file.deleted is False\n    file.delete()\n    assert file.deleted is True\n\n\ndef test_memory_file_repr():\n    \"\"\"MemoryFile has useful repr for debugging.\"\"\"\n    file = MemoryFile('/test/file.txt', content='content')\n    assert repr(file) == snapshot(\"MemoryFile(path=/test/file.txt, content='...', permissions=420)\")\n\n\ndef test_memory_file_bytes_repr():\n    \"\"\"MemoryFile repr shows b'...' for bytes content.\"\"\"\n    file = MemoryFile('/test/file.bin', content=b'\\x00')\n    assert repr(file) == snapshot(\"MemoryFile(path=/test/file.bin, content=b'...', permissions=420)\")\n\n\n# =============================================================================\n# CallbackFile Behavior\n# =============================================================================\n\n\ndef test_callback_file_read():\n    \"\"\"CallbackFile calls read callback.\"\"\"\n    read_calls: list[PurePosixPath] = []\n\n    def read_fn(path: PurePosixPath) -> str:\n        read_calls.append(path)\n        return f'content from {path}'\n\n    def write_fn(path: PurePosixPath, content: str | bytes) -> None:\n        pass\n\n    file = CallbackFile('/test/file.txt', read=read_fn, write=write_fn)\n    fs = OSAccess([file])\n\n    result = Monty('from pathlib import Path; Path(\"/test/file.txt\").read_text()').run(os=fs)\n    assert result == snapshot('content from /test/file.txt')\n    assert len(read_calls) == 1\n\n\ndef test_callback_file_write_direct():\n    \"\"\"CallbackFile calls write callback via direct API.\"\"\"\n    written: list[tuple[PurePosixPath, Any]] = []\n\n    def read_fn(path: PurePosixPath) -> str:\n        return ''\n\n    def write_fn(path: PurePosixPath, content: str | bytes) -> None:\n        written.append((path, content))\n\n    file = CallbackFile('/test/file.txt', read=read_fn, write=write_fn)\n    fs = OSAccess([file])\n\n    # Use direct API since write_text not implemented in Monty\n    fs.path_write_text(P('/test/file.txt'), 'new content')\n    assert len(written) == 1\n    assert written[0][1] == snapshot('new content')\n\n\ndef test_callback_file_custom_permissions():\n    \"\"\"CallbackFile accepts custom permissions.\"\"\"\n    file = CallbackFile(\n        '/test/file.txt',\n        read=lambda _: '',\n        write=lambda _p, _c: None,\n        permissions=0o700,\n    )\n    assert file.permissions == snapshot(0o700)\n\n\ndef test_callback_file_repr():\n    \"\"\"CallbackFile has useful repr for debugging.\"\"\"\n    file = CallbackFile('/test/file.txt', read=lambda _: '', write=lambda _, __: None)\n    assert 'CallbackFile(path=/test/file.txt' in repr(file)\n\n\n# =============================================================================\n# Custom AbstractFile Implementation\n# =============================================================================\n\n\nclass CustomFile:\n    \"\"\"Minimal custom AbstractFile implementation.\"\"\"\n\n    def __init__(self, path: str, content: str) -> None:\n        self.path = PurePosixPath(path)\n        self.name = self.path.name\n        self.permissions = 0o644\n        self.deleted = False\n        self.content = content\n\n    def read_content(self) -> str:\n        return self.content\n\n    def write_content(self, content: str | bytes) -> None:\n        self.content = content if isinstance(content, str) else content.decode()\n\n    def delete(self) -> None:\n        self.deleted = True\n\n\ndef test_custom_abstract_file():\n    \"\"\"Custom AbstractFile implementation works with OSAccess.\"\"\"\n    custom = CustomFile('/test/custom.txt', 'custom content')\n    fs = OSAccess([custom])\n\n    result = Monty('from pathlib import Path; Path(\"/test/custom.txt\").read_text()').run(os=fs)\n    assert result == snapshot('custom content')\n\n\ndef test_custom_abstract_file_mixed_with_memory_file():\n    \"\"\"Custom AbstractFile can be mixed with MemoryFile.\"\"\"\n    custom = CustomFile('/test/custom.txt', 'from custom')\n    memory = MemoryFile('/test/memory.txt', content='from memory')\n    fs = OSAccess([custom, memory])\n\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/test/custom.txt').read_text(), Path('/test/memory.txt').read_text())\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(('from custom', 'from memory'))\n\n\n# =============================================================================\n# Direct API Test (without Monty)\n# =============================================================================\n\n\ndef test_os_access_direct_api():\n    \"\"\"OSAccess methods can be called directly without Monty.\"\"\"\n    fs = OSAccess(\n        [\n            MemoryFile('/test/file.txt', content='hello'),\n            MemoryFile('/test/subdir/nested.txt', content='nested'),\n        ]\n    )\n\n    # Test path_exists\n    assert fs.path_exists(P('/test/file.txt')) is True\n    assert fs.path_exists(P('/missing')) is False\n\n    # Test path_is_file / path_is_dir\n    assert fs.path_is_file(P('/test/file.txt')) is True\n    assert fs.path_is_dir(P('/test/file.txt')) is False\n    assert fs.path_is_dir(P('/test/subdir')) is True\n    assert fs.path_is_file(P('/test/subdir')) is False\n\n    # Test path_read_text / path_read_bytes\n    assert fs.path_read_text(P('/test/file.txt')) == 'hello'\n    assert fs.path_read_bytes(P('/test/file.txt')) == b'hello'\n\n    # Test path_stat\n    stat = fs.path_stat(P('/test/file.txt'))\n    assert stat.st_size == 5\n\n    # Test path_iterdir\n    contents = fs.path_iterdir(P('/test'))\n    assert sorted(contents) == snapshot([PurePosixPath('/test/file.txt'), PurePosixPath('/test/subdir')])\n\n    # Test path_absolute\n    assert fs.path_absolute(P('relative')) == '/relative'\n    assert fs.path_absolute(P('/absolute')) == '/absolute'\n\n\n# =============================================================================\n# Edge Cases\n# =============================================================================\n\n\ndef test_root_directory():\n    \"\"\"Root directory '/' is handled correctly.\"\"\"\n    fs = OSAccess([MemoryFile('/file.txt', content='root file')])\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/').is_dir(), sorted([str(p) for p in Path('/').iterdir()]))\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot((True, ['/file.txt']))\n\n\ndef test_empty_file():\n    \"\"\"Empty file content is handled correctly.\"\"\"\n    fs = OSAccess([MemoryFile('/empty.txt', content='')])\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/empty.txt').read_text(), Path('/empty.txt').stat().st_size)\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot(('', 0))\n\n\ndef test_large_nested_path():\n    \"\"\"Deeply nested paths are handled correctly.\"\"\"\n    fs = OSAccess([MemoryFile('/a/b/c/d/e/f/g/h/i/j/file.txt', content='deep')])\n    code = \"\"\"\nfrom pathlib import Path\nPath('/a/b/c/d/e/f/g/h/i/j/file.txt').read_text()\n\"\"\"\n    result = Monty(code).run(os=fs)\n    assert result == snapshot('deep')\n\n\ndef test_special_characters_in_content():\n    \"\"\"Special characters in file content are handled correctly.\"\"\"\n    content = 'line1\\nline2\\ttab\\r\\nwindows'\n    fs = OSAccess([MemoryFile('/special.txt', content=content)])\n    result = Monty('from pathlib import Path; Path(\"/special.txt\").read_text()').run(os=fs)\n    assert result == snapshot('line1\\nline2\\ttab\\r\\nwindows')\n"
  },
  {
    "path": "crates/monty-python/tests/test_os_access_compat.py",
    "content": "\"\"\"OSAccess compatibility tests.\n\nThese tests verify that OSAccess (Monty's virtual filesystem) behaves identically\nto CPython's real filesystem operations. Each test runs twice - once with Monty\nusing OSAccess/MemoryFile and once with CPython using a real temp directory.\n\nThis ensures that code written for real filesystems works correctly in the\nsandboxed Monty environment.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom pathlib import Path\nfrom typing import Any, TypeAlias\n\nimport pytest\n\nfrom pydantic_monty import MemoryFile, Monty, OSAccess\n\n# Type alias for nested tree structure (file content or nested dict).\n# Using Any for the recursive dict value since Python's type system doesn't\n# handle recursive types well without TypedDict or Protocol.\nTreeDict: TypeAlias = 'dict[str, str | bytes | TreeDict]'\n\n\nclass CodeRunner(ABC):\n    \"\"\"Abstract interface for running Python code against a filesystem.\n\n    Implementations provide either a virtual filesystem (Monty+OSAccess) or\n    a real filesystem (CPython+temp directory) for compatibility testing.\n    \"\"\"\n\n    @abstractmethod\n    def write_file(self, path: str, content: str | bytes) -> None:\n        \"\"\"Add a file to the test filesystem setup.\n\n        Args:\n            path: Relative path for the file (e.g., 'test/file.txt')\n            content: File content as string or bytes\n        \"\"\"\n\n    @abstractmethod\n    def run_code(self, code: str) -> Any:\n        \"\"\"Run Python code and return the result.\n\n        The code can use Path('relative/path') and it will be resolved to the\n        appropriate root (OSAccess root or temp directory).\n\n        Args:\n            code: Python code to execute\n\n        Returns:\n            The result of the last expression in the code\n\n        Raises:\n            Exception: If the code raises an exception\n        \"\"\"\n\n    @abstractmethod\n    def tree(self) -> TreeDict:\n        \"\"\"Return a dict tree of files and their contents.\n\n        Returns:\n            Nested dict where keys are file/dir names and values are:\n            - str/bytes for file contents\n            - dict for subdirectories\n        \"\"\"\n\n    @abstractmethod\n    def set_environ(self, environ: dict[str, str]) -> None:\n        \"\"\"Set environment variables for the test.\n\n        Args:\n            environ: Dictionary of environment variable names to values\n        \"\"\"\n\n\nclass MontyRunner(CodeRunner):\n    \"\"\"CodeRunner implementation using Monty with OSAccess virtual filesystem.\"\"\"\n\n    def __init__(self) -> None:\n        self._files: list[MemoryFile] = []\n        self._environ: dict[str, str] = {}\n        self._os_access: OSAccess | None = None\n\n    def write_file(self, path: str, content: str | bytes) -> None:\n        # Use relative paths - OSAccess now supports them\n        self._files.append(MemoryFile(path, content=content))\n        # Reset OSAccess so it gets rebuilt with new files\n        self._os_access = None\n\n    def set_environ(self, environ: dict[str, str]) -> None:\n        self._environ = environ\n        # Reset OSAccess so it gets rebuilt with new environ\n        self._os_access = None\n\n    def _get_os_access(self) -> OSAccess:\n        if self._os_access is None:\n            self._os_access = OSAccess(self._files, environ=self._environ)\n        return self._os_access\n\n    def run_code(self, code: str) -> Any:\n        # Prepend imports - OSAccess now handles relative paths\n        wrapped_code = f'from pathlib import Path\\nimport os\\n{code}'\n        m = Monty(wrapped_code)\n        return m.run(os=self._get_os_access())\n\n    def tree(self) -> TreeDict:\n        result: TreeDict = {}\n\n        def add_to_tree(tree: TreeDict, parts: list[str], content: str | bytes) -> None:\n            if len(parts) == 1:\n                tree[parts[0]] = content\n            else:\n                if parts[0] not in tree:\n                    tree[parts[0]] = {}\n                sub: Any = tree[parts[0]]\n                if isinstance(sub, dict):\n                    add_to_tree(sub, parts[1:], content)  # type: ignore[arg-type]\n\n        # Build tree from all files\n        for file in self._files:\n            if file.deleted:\n                continue\n            path_parts = list(file.path.parts)\n            content = file.read_content()\n            add_to_tree(result, path_parts, content)\n\n        return result\n\n\nclass CPythonRunner(CodeRunner):\n    \"\"\"CodeRunner implementation using CPython with a real temp directory.\"\"\"\n\n    def __init__(self, tmp_path: Path) -> None:\n        self._root = tmp_path\n        self._environ: dict[str, str] = {}\n\n    def write_file(self, path: str, content: str | bytes) -> None:\n        full_path = self._root / path\n        full_path.parent.mkdir(parents=True, exist_ok=True)\n        if isinstance(content, bytes):\n            full_path.write_bytes(content)\n        else:\n            full_path.write_text(content)\n\n    def set_environ(self, environ: dict[str, str]) -> None:\n        self._environ = environ\n\n    def run_code(self, code: str) -> Any:\n        import ast\n        import types\n\n        # Map absolute paths (starting with /) to the temp directory\n        # This matches OSAccess behavior which normalizes relative paths to /\n        root = self._root\n\n        def rooted_path(p: str | Path) -> Path:\n            path = Path(p)\n            if path.is_absolute():\n                # Absolute path - strip leading / and map to root\n                return root / str(path).lstrip('/')\n            else:\n                # Relative path - prepend / then map to root\n                return root / p\n\n        # Create a mock os module with our environ\n        mock_os = types.SimpleNamespace()\n        mock_os.environ = self._environ\n\n        def getenv(key: str, default: str | None = None) -> str | None:\n            return self._environ.get(key, default)\n\n        mock_os.getenv = getenv\n\n        namespace: dict[str, Any] = {'Path': rooted_path, 'os': mock_os}\n        exec(code, namespace)\n\n        # Find the last expression result\n        tree = ast.parse(code)\n        if tree.body and isinstance(tree.body[-1], ast.Expr):\n            last_expr = ast.Expression(tree.body[-1].value)\n            compiled = compile(last_expr, '<string>', 'eval')\n            return eval(compiled, namespace)\n        return None\n\n    def tree(self) -> TreeDict:\n        def build_tree(path: Path) -> TreeDict:\n            result: TreeDict = {}\n            for item in sorted(path.iterdir()):\n                if item.is_dir():\n                    subtree = build_tree(item)\n                    result[item.name] = subtree\n                else:\n                    # Try to read as text, fall back to bytes\n                    try:\n                        result[item.name] = item.read_text()\n                    except UnicodeDecodeError:\n                        result[item.name] = item.read_bytes()\n            return result\n\n        if not self._root.exists():\n            return {}\n        return build_tree(self._root)\n\n\n@pytest.fixture(params=['monty', 'cpython'])\ndef runner(request: pytest.FixtureRequest, tmp_path: Path) -> CodeRunner:\n    \"\"\"Fixture that provides both Monty and CPython runners for comparison testing.\"\"\"\n    if request.param == 'monty':\n        return MontyRunner()\n    else:\n        return CPythonRunner(tmp_path)\n\n\n# =============================================================================\n# Path Existence Tests\n# =============================================================================\n\n\ndef test_path_exists_file(runner: CodeRunner) -> None:\n    \"\"\"Path.exists() returns True for existing files.\"\"\"\n    runner.write_file('test/file.txt', 'hello')\n    result = runner.run_code(\"Path('/test/file.txt').exists()\")\n    assert result is True\n\n\ndef test_path_exists_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.exists() returns True for directories.\"\"\"\n    runner.write_file('test/subdir/file.txt', 'hello')\n    result = runner.run_code(\"Path('/test/subdir').exists()\")\n    assert result is True\n\n\ndef test_path_exists_missing(runner: CodeRunner) -> None:\n    \"\"\"Path.exists() returns False for non-existent paths.\"\"\"\n    result = runner.run_code(\"Path('/missing/file.txt').exists()\")\n    assert result is False\n\n\ndef test_path_is_file(runner: CodeRunner) -> None:\n    \"\"\"Path.is_file() returns True for files, False for directories.\"\"\"\n    runner.write_file('test/file.txt', 'hello')\n    assert runner.run_code(\"Path('/test/file.txt').is_file()\") is True\n    assert runner.run_code(\"Path('/test').is_file()\") is False\n\n\ndef test_path_is_dir(runner: CodeRunner) -> None:\n    \"\"\"Path.is_dir() returns True for directories, False for files.\"\"\"\n    runner.write_file('test/file.txt', 'hello')\n    assert runner.run_code(\"Path('/test').is_dir()\") is True\n    assert runner.run_code(\"Path('/test/file.txt').is_dir()\") is False\n\n\n# =============================================================================\n# Reading Files\n# =============================================================================\n\n\ndef test_read_text(runner: CodeRunner) -> None:\n    \"\"\"Path.read_text() returns file content as string.\"\"\"\n    runner.write_file('data/hello.txt', 'hello world')\n    result = runner.run_code(\"Path('/data/hello.txt').read_text()\")\n    assert result == 'hello world'\n\n\ndef test_read_bytes(runner: CodeRunner) -> None:\n    \"\"\"Path.read_bytes() returns file content as bytes.\"\"\"\n    runner.write_file('data/binary.bin', b'\\x00\\x01\\x02\\x03')\n    result = runner.run_code(\"Path('/data/binary.bin').read_bytes()\")\n    assert result == b'\\x00\\x01\\x02\\x03'\n\n\ndef test_read_text_unicode(runner: CodeRunner) -> None:\n    \"\"\"Path.read_text() handles unicode content.\"\"\"\n    runner.write_file('unicode.txt', 'hello \\u2603 world')\n    result = runner.run_code(\"Path('/unicode.txt').read_text()\")\n    assert result == 'hello \\u2603 world'\n\n\n# =============================================================================\n# Tree Verification\n# =============================================================================\n\n\ndef test_tree_simple(runner: CodeRunner) -> None:\n    \"\"\"tree() returns correct structure for simple files.\"\"\"\n    runner.write_file('a.txt', 'content a')\n    runner.write_file('b.txt', 'content b')\n    assert runner.tree() == {'a.txt': 'content a', 'b.txt': 'content b'}\n\n\ndef test_tree_nested(runner: CodeRunner) -> None:\n    \"\"\"tree() returns correct structure for nested directories.\"\"\"\n    runner.write_file('dir/subdir/file.txt', 'nested content')\n    assert runner.tree() == {'dir': {'subdir': {'file.txt': 'nested content'}}}\n\n\ndef test_tree_mixed(runner: CodeRunner) -> None:\n    \"\"\"tree() handles mixed files and directories.\"\"\"\n    runner.write_file('root.txt', 'root')\n    runner.write_file('dir/file.txt', 'in dir')\n    expected = {'root.txt': 'root', 'dir': {'file.txt': 'in dir'}}\n    assert runner.tree() == expected\n\n\n# =============================================================================\n# Stat Operations\n# =============================================================================\n\n\ndef test_stat_size(runner: CodeRunner) -> None:\n    \"\"\"Path.stat().st_size returns correct file size.\"\"\"\n    runner.write_file('sized.txt', 'hello')\n    result = runner.run_code(\"Path('/sized.txt').stat().st_size\")\n    assert result == 5\n\n\ndef test_stat_size_unicode(runner: CodeRunner) -> None:\n    \"\"\"Path.stat().st_size returns byte size for unicode content.\"\"\"\n    # Unicode snowman is 3 bytes in UTF-8\n    runner.write_file('unicode.txt', '\\u2603')\n    result = runner.run_code(\"Path('/unicode.txt').stat().st_size\")\n    assert result == 3\n\n\n# =============================================================================\n# Directory Listing\n# =============================================================================\n\n\ndef test_iterdir(runner: CodeRunner) -> None:\n    \"\"\"Path.iterdir() lists directory contents.\n\n    Note: Monty returns filenames as strings while CPython returns Path objects\n    with full paths. We normalize by getting .name (or using the string directly\n    for Monty). Sorting is done in Python due to Monty limitations.\n    \"\"\"\n    runner.write_file('dir/a.txt', 'a')\n    runner.write_file('dir/b.txt', 'b')\n    runner.write_file('dir/subdir/c.txt', 'c')\n    # Get filenames - Monty returns strings, CPython returns Paths with full path\n    # Use list() to collect, then sort in Python\n    result = runner.run_code(\"list(Path('/dir').iterdir())\")\n    # Normalize: Monty gives strings, CPython gives Paths\n    if isinstance(result[0], str):\n        names = result  # Monty: already filenames\n    else:\n        names = [p.name for p in result]  # CPython: extract name from Path\n    assert sorted(names) == ['a.txt', 'b.txt', 'subdir']\n\n\n# =============================================================================\n# Error Cases - FileNotFoundError\n# =============================================================================\n\n\ndef test_read_text_file_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.read_text() raises FileNotFoundError for missing files.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/missing.txt').read_text()\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_read_bytes_file_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.read_bytes() raises FileNotFoundError for missing files.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/missing.bin').read_bytes()\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_stat_file_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.stat() raises FileNotFoundError for missing files.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/missing.txt').stat()\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_iterdir_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.iterdir() raises FileNotFoundError for missing directories.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    list(Path('/missing_dir').iterdir())\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\n# =============================================================================\n# Error Cases - IsADirectoryError\n# =============================================================================\n\n\ndef test_read_text_is_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.read_text() raises IsADirectoryError when path is a directory.\"\"\"\n    runner.write_file('mydir/file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/mydir').read_text()\nexcept IsADirectoryError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'IsADirectoryError'\n\n\ndef test_read_bytes_is_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.read_bytes() raises IsADirectoryError when path is a directory.\"\"\"\n    runner.write_file('mydir/file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/mydir').read_bytes()\nexcept IsADirectoryError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'IsADirectoryError'\n\n\n# =============================================================================\n# Error Cases - NotADirectoryError\n# =============================================================================\n\n\ndef test_iterdir_not_a_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.iterdir() raises NotADirectoryError when path is a file.\"\"\"\n    runner.write_file('file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    list(Path('/file.txt').iterdir())\nexcept NotADirectoryError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'NotADirectoryError'\n\n\n# =============================================================================\n# Error Cases - FileExistsError\n# =============================================================================\n\n\ndef test_mkdir_file_exists(runner: CodeRunner) -> None:\n    \"\"\"Path.mkdir() raises FileExistsError when directory already exists.\"\"\"\n    runner.write_file('existing_dir/file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/existing_dir').mkdir()\nexcept FileExistsError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileExistsError'\n\n\ndef test_mkdir_file_at_path(runner: CodeRunner) -> None:\n    \"\"\"Path.mkdir() raises FileExistsError when a file exists at the path.\"\"\"\n    runner.write_file('somefile.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/somefile.txt').mkdir()\nexcept FileExistsError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileExistsError'\n\n\ndef test_mkdir_exist_ok_no_error(runner: CodeRunner) -> None:\n    \"\"\"Path.mkdir(exist_ok=True) doesn't raise when directory exists.\"\"\"\n    runner.write_file('existing_dir/file.txt', 'content')\n    result = runner.run_code(\"\"\"\nPath('/existing_dir').mkdir(exist_ok=True)\n'no error'\n\"\"\")\n    assert result == 'no error'\n\n\n# =============================================================================\n# Error Cases - mkdir parent not found\n# =============================================================================\n\n\ndef test_mkdir_parent_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.mkdir() raises FileNotFoundError when parent doesn't exist.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/no/parent/here').mkdir()\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_mkdir_parents_creates_all(runner: CodeRunner) -> None:\n    \"\"\"Path.mkdir(parents=True) creates all parent directories.\"\"\"\n    result = runner.run_code(\"\"\"\nPath('/a/b/c/d').mkdir(parents=True)\nPath('/a/b/c/d').is_dir()\n\"\"\")\n    assert result is True\n\n\n# =============================================================================\n# Error Cases - unlink\n# =============================================================================\n\n\ndef test_unlink_file_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.unlink() raises FileNotFoundError for missing files.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/missing.txt').unlink()\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_unlink_is_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.unlink() raises an error when path is a directory.\n\n    Note: On macOS, CPython raises PermissionError for unlink() on directories,\n    while Linux raises IsADirectoryError. OSAccess consistently raises IsADirectoryError.\n    \"\"\"\n    runner.write_file('mydir/file.txt', 'content')\n    # Use OSError as catch-all since PermissionError and IsADirectoryError are subclasses\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/mydir').unlink()\nexcept OSError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    # OSAccess raises IsADirectoryError, CPython on macOS raises PermissionError\n    assert result in ('IsADirectoryError', 'PermissionError')\n\n\n# =============================================================================\n# Error Cases - rmdir\n# =============================================================================\n\n\ndef test_rmdir_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.rmdir() raises FileNotFoundError for missing directories.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/missing_dir').rmdir()\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_rmdir_not_a_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.rmdir() raises NotADirectoryError when path is a file.\"\"\"\n    runner.write_file('file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/file.txt').rmdir()\nexcept NotADirectoryError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'NotADirectoryError'\n\n\ndef test_rmdir_not_empty(runner: CodeRunner) -> None:\n    \"\"\"Path.rmdir() raises OSError when directory is not empty.\"\"\"\n    runner.write_file('nonempty/file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/nonempty').rmdir()\nexcept OSError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'OSError'\n\n\n# =============================================================================\n# Error Cases - rename\n# =============================================================================\n\n\ndef test_rename_source_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.rename() raises FileNotFoundError when source doesn't exist.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/missing.txt').rename(Path('/new.txt'))\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\n# =============================================================================\n# Write Operations\n# =============================================================================\n\n\ndef test_write_text_new_file(runner: CodeRunner) -> None:\n    \"\"\"Path.write_text() creates a new file and returns character count.\"\"\"\n    result = runner.run_code(\"\"\"\ncount = Path('/new_file.txt').write_text('hello world')\n(count, Path('/new_file.txt').read_text())\n\"\"\")\n    assert result == (11, 'hello world')\n\n\ndef test_write_text_overwrite(runner: CodeRunner) -> None:\n    \"\"\"Path.write_text() overwrites existing files.\"\"\"\n    runner.write_file('existing.txt', 'old content')\n    result = runner.run_code(\"\"\"\nPath('/existing.txt').write_text('new content')\nPath('/existing.txt').read_text()\n\"\"\")\n    assert result == 'new content'\n\n\ndef test_write_bytes_new_file(runner: CodeRunner) -> None:\n    \"\"\"Path.write_bytes() creates a new file and returns byte count.\"\"\"\n    result = runner.run_code(\"\"\"\ncount = Path('/new_binary.bin').write_bytes(b'\\\\x00\\\\x01\\\\x02')\n(count, Path('/new_binary.bin').read_bytes())\n\"\"\")\n    assert result == (3, b'\\x00\\x01\\x02')\n\n\ndef test_write_text_parent_not_found(runner: CodeRunner) -> None:\n    \"\"\"Path.write_text() raises FileNotFoundError when parent doesn't exist.\"\"\"\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/no/parent/file.txt').write_text('content')\nexcept FileNotFoundError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'FileNotFoundError'\n\n\ndef test_write_text_to_directory(runner: CodeRunner) -> None:\n    \"\"\"Path.write_text() raises IsADirectoryError when writing to a directory.\"\"\"\n    runner.write_file('mydir/file.txt', 'content')\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    Path('/mydir').write_text('content')\nexcept IsADirectoryError as e:\n    result = type(e).__name__\nresult\n\"\"\")\n    assert result == 'IsADirectoryError'\n\n\n# =============================================================================\n# Environment Variable Tests\n# =============================================================================\n\n\ndef test_environ_key_access(runner: CodeRunner) -> None:\n    \"\"\"os.environ['KEY'] returns the value for existing keys.\"\"\"\n    runner.set_environ({'MY_VAR': 'my_value'})\n    result = runner.run_code(\"os.environ['MY_VAR']\")\n    assert result == 'my_value'\n\n\ndef test_environ_get_method(runner: CodeRunner) -> None:\n    \"\"\"os.environ.get() returns the value for existing keys.\"\"\"\n    runner.set_environ({'MY_VAR': 'my_value'})\n    result = runner.run_code(\"os.environ.get('MY_VAR')\")\n    assert result == 'my_value'\n\n\ndef test_environ_get_missing_with_default(runner: CodeRunner) -> None:\n    \"\"\"os.environ.get() returns default for missing keys.\"\"\"\n    runner.set_environ({})\n    result = runner.run_code(\"os.environ.get('MISSING', 'fallback')\")\n    assert result == 'fallback'\n\n\ndef test_environ_missing_key_raises_keyerror(runner: CodeRunner) -> None:\n    \"\"\"os.environ['MISSING'] raises KeyError with consistent message.\"\"\"\n    runner.set_environ({})\n    result = runner.run_code(\"\"\"\nresult = None\ntry:\n    os.environ['NONEXISTENT_KEY']\nexcept KeyError as e:\n    result = str(e)\nresult\n\"\"\")\n    # Both Monty and CPython should produce the same KeyError message format\n    assert result == \"'NONEXISTENT_KEY'\"\n"
  },
  {
    "path": "crates/monty-python/tests/test_os_access_raw.py",
    "content": "\"\"\"Tests for AbstractFileSystem implementation.\n\nThese tests verify that AbstractFileSystem can be subclassed to provide\na virtual filesystem that Monty code can interact with via Path methods.\n\"\"\"\n\nfrom pathlib import PurePosixPath\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\nfrom pydantic_monty import AbstractOS, StatResult\n\n\nclass TestOS(AbstractOS):\n    \"\"\"A simple in-memory filesystem for testing.\"\"\"\n\n    __test__ = False\n\n    def __init__(self) -> None:\n        self.files: dict[str, bytes] = {}\n        self.directories: set[str] = {'/'}\n\n    def _ensure_parent_exists(self, path: str) -> None:\n        \"\"\"Ensure all parent directories exist.\"\"\"\n        parts = path.rstrip('/').split('/')\n        for i in range(1, len(parts)):\n            parent = '/'.join(parts[:i]) or '/'\n            self.directories.add(parent)\n\n    def path_exists(self, path: PurePosixPath) -> bool:\n        p = str(path)\n        return p in self.files or p in self.directories\n\n    def path_is_file(self, path: PurePosixPath) -> bool:\n        return str(path) in self.files\n\n    def path_is_dir(self, path: PurePosixPath) -> bool:\n        return str(path) in self.directories\n\n    def path_is_symlink(self, path: PurePosixPath) -> bool:\n        return False  # No symlink support in this simple implementation\n\n    def path_read_text(self, path: PurePosixPath) -> str:\n        p = str(path)\n        if p not in self.files:\n            raise FileNotFoundError(f'No such file: {p}')\n        return self.files[p].decode('utf-8')\n\n    def path_read_bytes(self, path: PurePosixPath) -> bytes:\n        p = str(path)\n        if p not in self.files:\n            raise FileNotFoundError(f'No such file: {p}')\n        return self.files[p]\n\n    def path_write_text(self, path: PurePosixPath, data: str) -> int:\n        p = str(path)\n        self._ensure_parent_exists(p)\n        self.files[p] = data.encode('utf-8')\n        return len(data)\n\n    def path_write_bytes(self, path: PurePosixPath, data: bytes) -> int:\n        p = str(path)\n        self._ensure_parent_exists(p)\n        self.files[p] = data\n        return len(data)\n\n    def path_mkdir(self, path: PurePosixPath, parents: bool, exist_ok: bool) -> None:\n        p = str(path)\n        if p in self.directories:\n            if not exist_ok:\n                raise FileExistsError(f'Directory exists: {p}')\n            return\n        if parents:\n            self._ensure_parent_exists(p)\n        self.directories.add(p)\n\n    def path_unlink(self, path: PurePosixPath) -> None:\n        p = str(path)\n        if p not in self.files:\n            raise FileNotFoundError(f'No such file: {p}')\n        del self.files[p]\n\n    def path_rmdir(self, path: PurePosixPath) -> None:\n        p = str(path)\n        if p not in self.directories:\n            raise FileNotFoundError(f'No such directory: {p}')\n        # Check if directory is empty\n        for f in self.files:\n            if f.startswith(p + '/'):\n                raise OSError(f'Directory not empty: {p}')\n        for d in self.directories:\n            if d != p and d.startswith(p + '/'):\n                raise OSError(f'Directory not empty: {p}')\n        self.directories.remove(p)\n\n    def path_iterdir(self, path: PurePosixPath) -> list[PurePosixPath]:\n        p = str(path)\n        if p not in self.directories:\n            raise FileNotFoundError(f'No such directory: {p}')\n        result: list[PurePosixPath] = []\n        prefix = p.rstrip('/') + '/'\n        seen: set[str] = set()\n        for f in self.files:\n            if f.startswith(prefix):\n                # Get immediate child name\n                rest = f[len(prefix) :]\n                child = rest.split('/')[0]\n                if child and child not in seen:\n                    seen.add(child)\n                    result.append(PurePosixPath(prefix + child))\n        for d in self.directories:\n            if d.startswith(prefix) and d != p:\n                rest = d[len(prefix) :]\n                child = rest.split('/')[0]\n                if child and child not in seen:\n                    seen.add(child)\n                    result.append(PurePosixPath(prefix + child))\n        return sorted(result)\n\n    def path_stat(self, path: PurePosixPath) -> StatResult:\n        p = str(path)\n        if p in self.files:\n            return StatResult.file_stat(len(self.files[p]), 0o644, 0.0)\n        elif p in self.directories:\n            return StatResult.dir_stat(0o755, 0.0)\n        else:\n            raise FileNotFoundError(f'No such file or directory: {p}')\n\n    def path_rename(self, path: PurePosixPath, target: PurePosixPath) -> None:\n        p = str(path)\n        t = str(target)\n        if p in self.files:\n            self._ensure_parent_exists(t)\n            self.files[t] = self.files.pop(p)\n        elif p in self.directories:\n            self._ensure_parent_exists(t)\n            self.directories.remove(p)\n            self.directories.add(t)\n            # Move all files under this directory\n            prefix = p.rstrip('/') + '/'\n            to_move = [(f, t + f[len(p) :]) for f in self.files if f.startswith(prefix)]\n            for old, new in to_move:\n                self.files[new] = self.files.pop(old)\n        else:\n            raise FileNotFoundError(f'No such file or directory: {p}')\n\n    def path_resolve(self, path: PurePosixPath) -> str:\n        # Simple implementation: just normalize the path\n        p = str(path)\n        parts: list[str] = []\n        for part in p.split('/'):\n            if part == '..':\n                if parts:\n                    parts.pop()\n            elif part and part != '.':\n                parts.append(part)\n        return '/' + '/'.join(parts)\n\n    def path_absolute(self, path: PurePosixPath) -> str:\n        p = str(path)\n        if p.startswith('/'):\n            return p\n        return '/' + p\n\n    def getenv(self, key: str, default: str | None = None) -> str | None:\n        # Simple virtual environment for testing\n        env = {\n            'TEST_VAR': 'test_value',\n            'HOME': '/test/home',\n        }\n        return env.get(key, default)\n\n    def get_environ(self) -> dict[str, str]:\n        return {\n            'TEST_VAR': 'test_value',\n            'HOME': '/test/home',\n        }\n\n\n# =============================================================================\n# Basic AbstractFileSystem tests\n# =============================================================================\n\n\ndef test_abstract_filesystem_exists():\n    \"\"\"AbstractFileSystem.path_exists() works with os.\"\"\"\n    fs = TestOS()\n    fs.files['/test.txt'] = b'hello'\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/test.txt\").exists()')\n    result = m.run(os=fs)\n\n    assert result is True\n\n\ndef test_abstract_filesystem_exists_missing():\n    \"\"\"AbstractFileSystem.path_exists() returns False for missing files.\"\"\"\n    fs = TestOS()\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/missing.txt\").exists()')\n    result = m.run(os=fs)\n\n    assert result is False\n\n\ndef test_abstract_filesystem_is_file():\n    \"\"\"AbstractFileSystem.path_is_file() distinguishes files from directories.\"\"\"\n    fs = TestOS()\n    fs.files['/file.txt'] = b'content'\n    fs.directories.add('/mydir')\n\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/file.txt').is_file(), Path('/mydir').is_file())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot((True, False))\n\n\ndef test_abstract_filesystem_is_dir():\n    \"\"\"AbstractFileSystem.path_is_dir() distinguishes directories from files.\"\"\"\n    fs = TestOS()\n    fs.files['/file.txt'] = b'content'\n    fs.directories.add('/mydir')\n\n    code = \"\"\"\nfrom pathlib import Path\n(Path('/file.txt').is_dir(), Path('/mydir').is_dir())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot((False, True))\n\n\ndef test_abstract_filesystem_read_text():\n    \"\"\"AbstractFileSystem.path_read_text() returns file contents.\"\"\"\n    fs = TestOS()\n    fs.files['/hello.txt'] = b'Hello, World!'\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/hello.txt\").read_text()')\n    result = m.run(os=fs)\n\n    assert result == snapshot('Hello, World!')\n\n\ndef test_abstract_filesystem_read_text_missing():\n    \"\"\"AbstractFileSystem.path_read_text() raises FileNotFoundError for missing files.\"\"\"\n    fs = TestOS()\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/missing.txt\").read_text()')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(os=fs)\n    assert str(exc_info.value) == snapshot('FileNotFoundError: No such file: /missing.txt')\n    assert isinstance(exc_info.value.exception(), FileNotFoundError)\n\n\ndef test_abstract_filesystem_read_bytes():\n    \"\"\"AbstractFileSystem.path_read_bytes() returns raw bytes.\"\"\"\n    fs = TestOS()\n    fs.files['/data.bin'] = b'\\x00\\x01\\x02\\x03'\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/data.bin\").read_bytes()')\n    result = m.run(os=fs)\n\n    assert result == snapshot(b'\\x00\\x01\\x02\\x03')\n\n\n# =============================================================================\n# stat() tests\n# =============================================================================\n\n\ndef test_abstract_filesystem_stat_file():\n    \"\"\"AbstractFileSystem.path_stat() returns stat result for files.\"\"\"\n    fs = TestOS()\n    fs.files['/file.txt'] = b'hello world'\n\n    code = \"\"\"\nfrom pathlib import Path\ns = Path('/file.txt').stat()\n(s.st_size, s.st_mode)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot((11, 0o100644))\n\n\ndef test_abstract_filesystem_stat_directory():\n    \"\"\"AbstractFileSystem.path_stat() returns stat result for directories.\"\"\"\n    fs = TestOS()\n    fs.directories.add('/mydir')\n\n    code = \"\"\"\nfrom pathlib import Path\ns = Path('/mydir').stat()\ns.st_mode\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot(0o040755)\n\n\ndef test_abstract_filesystem_stat_missing():\n    \"\"\"AbstractFileSystem.path_stat() raises FileNotFoundError for missing paths.\"\"\"\n    fs = TestOS()\n\n    m = pydantic_monty.Monty('from pathlib import Path\\nPath(\"/missing\").stat()')\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(os=fs)\n\n    assert str(exc_info.value) == snapshot('FileNotFoundError: No such file or directory: /missing')\n    assert exc_info.value.display() == snapshot(\"\"\"\\\nTraceback (most recent call last):\n  File \"main.py\", line 2, in <module>\n    Path(\"/missing\").stat()\n    ~~~~~~~~~~~~~~~~~~~~~~~\nFileNotFoundError: No such file or directory: /missing\\\n\"\"\")\n\n\n# =============================================================================\n# iterdir() tests\n# =============================================================================\n\n\ndef test_abstract_filesystem_iterdir():\n    \"\"\"AbstractFileSystem.path_iterdir() lists directory contents.\"\"\"\n    fs = TestOS()\n    fs.directories.add('/mydir')\n    fs.files['/mydir/a.txt'] = b'a'\n    fs.files['/mydir/b.txt'] = b'b'\n    fs.directories.add('/mydir/subdir')\n\n    code = \"\"\"\nfrom pathlib import Path\nlist(Path('/mydir').iterdir())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    # Result is a list of Path objects with child names joined to parent\n    assert len(result) == 3\n    names = sorted(str(p) for p in result)\n    assert names == snapshot(['/mydir/a.txt', '/mydir/b.txt', '/mydir/subdir'])\n\n\ndef test_abstract_filesystem_iterdir_empty():\n    \"\"\"AbstractFileSystem.path_iterdir() returns empty list for empty directory.\"\"\"\n    fs = TestOS()\n    fs.directories.add('/empty')\n\n    code = \"\"\"\nfrom pathlib import Path\nlist(Path('/empty').iterdir())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot([])\n\n\n# =============================================================================\n# resolve() and absolute() tests\n# =============================================================================\n\n\ndef test_abstract_filesystem_resolve():\n    \"\"\"AbstractFileSystem.path_resolve() normalizes paths.\"\"\"\n    fs = TestOS()\n\n    code = \"\"\"\nfrom pathlib import Path\nstr(Path('/foo/bar/../baz').resolve())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot('/foo/baz')\n\n\ndef test_abstract_filesystem_absolute():\n    \"\"\"AbstractFileSystem.path_absolute() returns absolute path.\"\"\"\n    fs = TestOS()\n\n    code = \"\"\"\nfrom pathlib import Path\nstr(Path('/already/absolute').absolute())\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot('/already/absolute')\n\n\ndef test_abstract_filesystem_getenv():\n    \"\"\"AbstractFileSystem.getenv() returns environment variable value.\"\"\"\n    fs = TestOS()\n\n    code = \"\"\"\nimport os\nos.getenv('TEST_VAR')\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot('test_value')\n\n\ndef test_abstract_filesystem_getenv_missing():\n    \"\"\"AbstractFileSystem.getenv() returns None for missing variable.\"\"\"\n    fs = TestOS()\n\n    code = \"\"\"\nimport os\nos.getenv('NONEXISTENT')\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result is None\n\n\ndef test_abstract_filesystem_getenv_default():\n    \"\"\"AbstractFileSystem.getenv() returns default for missing variable.\"\"\"\n    fs = TestOS()\n\n    code = \"\"\"\nimport os\nos.getenv('NONEXISTENT', 'my_default')\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=fs)\n\n    assert result == snapshot('my_default')\n\n\n# =============================================================================\n# file_stat / dir_stat helper tests\n# =============================================================================\n\n\ndef test_file_stat_helper():\n    \"\"\"file_stat() creates a proper stat result.\"\"\"\n    stat = StatResult.file_stat(1024, 0o644, 1700000000.0)\n\n    # Check it has the expected structure (10 fields)\n    assert len(stat) == snapshot(10)\n    # Index access: st_mode=0, st_size=6, st_mtime=8\n    assert stat[0] == snapshot(0o100644)  # st_mode - file_stat adds file type bits\n    assert stat[6] == snapshot(1024)  # st_size\n    assert stat[8] == snapshot(1700000000.0)  # st_mtime\n\n\ndef test_dir_stat_helper():\n    \"\"\"dir_stat() creates a proper stat result for directories.\"\"\"\n    stat = StatResult.dir_stat(0o755, 1700000000.0)\n\n    assert len(stat) == snapshot(10)\n    # Index access: st_mode=0, st_size=6, st_mtime=8\n    assert stat[0] == snapshot(0o040755)  # st_mode - dir_stat adds directory type bits\n    assert stat[6] == snapshot(4096)  # st_size - directories have fixed size\n    assert stat[8] == snapshot(1700000000.0)  # st_mtime\n\n\ndef test_path_monty_to_py():\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/foo/bar/thing.txt\")')\n    result = m.run()\n    assert result == PurePosixPath('/foo/bar/thing.txt')\n    assert type(result) is PurePosixPath\n\n\ndef test_path_py_to_monty():\n    p = PurePosixPath('/foo/bar/thing.txt')\n    m = pydantic_monty.Monty('f\"type={type(p)} {p=}\"', inputs=['p'])\n    result = m.run(inputs={'p': p})\n    assert result == snapshot(\"type=<class 'PosixPath'> p=PosixPath('/foo/bar/thing.txt')\")\n"
  },
  {
    "path": "crates/monty-python/tests/test_os_calls.py",
    "content": "\"\"\"Tests for OS function calls (Path methods) via the start/resume API.\n\nThese tests verify that Path filesystem methods correctly yield OS calls\nwith the right function name and arguments, and that return values from\nthe host are properly converted and used by Monty code.\n\"\"\"\n\nfrom pathlib import PurePosixPath\nfrom typing import Any\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\nfrom pydantic_monty import StatResult\n\n# =============================================================================\n# Basic OS call yielding\n# =============================================================================\n\n\ndef test_path_exists_yields_oscall():\n    \"\"\"Path.exists() yields an OS call with correct function and path.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/test.txt\").exists()')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.exists')\n    assert result.args == snapshot((PurePosixPath('/tmp/test.txt'),))\n    assert result.kwargs == snapshot({})\n\n\ndef test_path_stat_yields_oscall():\n    \"\"\"Path.stat() yields an OS call.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/etc/passwd\").stat()')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.stat')\n    assert result.args == snapshot((PurePosixPath('/etc/passwd'),))\n\n\ndef test_path_read_text_yields_oscall():\n    \"\"\"Path.read_text() yields an OS call.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/hello.txt\").read_text()')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.read_text')\n    assert result.args == snapshot((PurePosixPath('/tmp/hello.txt'),))\n\n\n# =============================================================================\n# Path construction and concatenation\n# =============================================================================\n\n\ndef test_path_concatenation():\n    \"\"\"Path concatenation with / operator produces correct path string.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\nbase = Path('/home')\nfull = base / 'user' / 'documents' / 'file.txt'\nfull.exists()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.args == snapshot((PurePosixPath('/home/user/documents/file.txt'),))\n\n\n# =============================================================================\n# Resume with return values\n# =============================================================================\n\n\ndef test_exists_resume():\n    \"\"\"Resuming exists() with bool returns it to Monty code.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/test.txt\").exists()')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=True)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output is True\n\n\ndef test_read_text_resume():\n    \"\"\"Resuming read_text() with string content returns it to Monty code.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\ncontent = Path('/tmp/hello.txt').read_text()\n'Content: ' + content\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value='Hello, World!')\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot('Content: Hello, World!')\n\n\n# =============================================================================\n# stat() result round-trip (Python -> Monty -> Python)\n# =============================================================================\n\n\ndef test_stat_resume_and_use_in_monty():\n    \"\"\"Resuming stat() with file_stat() allows Monty to access fields.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\ninfo = Path('/tmp/file.txt').stat()\n(info.st_mode, info.st_size, info[6])\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    assert snapshot_result.function_name == snapshot('Path.stat')\n\n    # Resume with a file_stat result - Monty accesses multiple fields\n    result = snapshot_result.resume(return_value=StatResult.file_stat(1024, 0o100_644, 1234567890.0))\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    # st_mode=0o100_644, st_size=1024, info[6]=st_size=1024\n    assert result.output == snapshot((0o100_644, 1024, 1024))\n\n\ndef test_stat_result_returned_from_monty():\n    \"\"\"stat_result returned from Monty is accessible in Python.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\nPath('/tmp/file.txt').stat()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=StatResult.file_stat(2048, 0o100_755, 1700000000.0))\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    stat_result = result.output\n\n    # Access attributes on the returned namedtuple\n    assert stat_result.st_mode == snapshot(0o100_755)\n    assert stat_result.st_size == snapshot(2048)\n    assert stat_result.st_mtime == snapshot(1700000000.0)\n\n    # Index access works too\n    assert stat_result[0] == snapshot(0o100_755)  # st_mode\n    assert stat_result[6] == snapshot(2048)  # st_size\n\n\ndef test_stat_result():\n    \"\"\"stat_result repr shows field names and values.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\nPath('/tmp/file.txt').stat()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=StatResult.file_stat(512, 0o644, 0.0))\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert repr(result.output) == snapshot(\n        'StatResult(st_mode=33188, st_ino=0, st_dev=0, st_nlink=1, st_uid=0, st_gid=0, st_size=512, st_atime=0.0, st_mtime=0.0, st_ctime=0.0)'\n    )\n    # Should be a tuple subclass\n    assert len(result.output) == 10\n    assert isinstance(result.output, tuple)\n\n\n# =============================================================================\n# Multiple OS calls in sequence\n# =============================================================================\n\n\ndef test_multiple_path_calls():\n    \"\"\"Multiple Path method calls yield multiple OS calls in sequence.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\np = Path('/tmp/test.txt')\nexists = p.exists()\nis_file = p.is_file()\n(exists, is_file)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    # First call: exists()\n    result = m.start()\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.function_name == snapshot('Path.exists')\n\n    # Resume exists() with True\n    result = result.resume(return_value=True)\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.function_name == snapshot('Path.is_file')\n\n    # Resume is_file() with True\n    result = result.resume(return_value=True)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot((True, True))\n\n\ndef test_conditional_path_calls():\n    \"\"\"Path calls inside conditionals work correctly.\"\"\"\n    code = \"\"\"\nfrom pathlib import Path\np = Path('/tmp/test.txt')\nif p.exists():\n    content = p.read_text()\nelse:\n    content = 'not found'\ncontent\n\"\"\"\n    m = pydantic_monty.Monty(code)\n\n    # First call: exists()\n    result = m.start()\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.function_name == snapshot('Path.exists')\n\n    # Resume exists() with True - should trigger read_text()\n    result = result.resume(return_value=True)\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.function_name == snapshot('Path.read_text')\n\n    # Resume read_text() with content\n    result = result.resume(return_value='file contents')\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot('file contents')\n\n\n# =============================================================================\n# OS call vs external function distinction\n# =============================================================================\n\n\ndef test_os_call_vs_external_function():\n    \"\"\"OS calls have is_os_function=True, external functions have is_os_function=False.\"\"\"\n    # OS call\n    m1 = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp\").exists()')\n    result1 = m1.start()\n    assert isinstance(result1, pydantic_monty.FunctionSnapshot)\n    assert result1.is_os_function is True\n\n    # External function\n    m2 = pydantic_monty.Monty('my_func()')\n    result2 = m2.start()\n    assert isinstance(result2, pydantic_monty.FunctionSnapshot)\n    assert result2.is_os_function is False\n\n\n# =============================================================================\n# os in run() method\n# =============================================================================\n\n\ndef test_os_basic():\n    \"\"\"os receives function name and args, return value is used.\"\"\"\n    calls: list[Any] = []\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> bool:\n        calls.append((function_name, args))\n        return True\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/test.txt\").exists()')\n    result = m.run(os=os_handler)\n\n    assert result is True\n    assert calls == snapshot([('Path.exists', (PurePosixPath('/tmp/test.txt'),))])\n\n\ndef test_os_stat():\n    \"\"\"os can return stat_result for Path.stat().\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'Path.stat':\n            return StatResult.file_stat(1024, 0o644, 1700000000.0)\n        return None\n\n    code = \"\"\"\nfrom pathlib import Path\ninfo = Path('/tmp/file.txt').stat()\n(info.st_mode, info.st_size)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=os_handler)\n\n    assert result == snapshot((0o100_644, 1024))\n\n\ndef test_os_multiple_calls():\n    \"\"\"os is called for each OS operation.\"\"\"\n    calls: list[Any] = []\n\n    def os_handler(\n        function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None\n    ) -> bool | str | None:\n        calls.append(function_name)\n        match function_name:\n            case 'Path.exists':\n                return True\n            case 'Path.read_text':\n                return 'file contents'\n            case _:\n                return None\n\n    code = \"\"\"\nfrom pathlib import Path\np = Path('/tmp/test.txt')\nif p.exists():\n    result = p.read_text()\nelse:\n    result = 'not found'\nresult\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=os_handler)\n\n    assert result == snapshot('file contents')\n    assert calls == snapshot(['Path.exists', 'Path.read_text'])\n\n\ndef test_os_not_provided_error():\n    \"\"\"Error is raised when OS call is made without os.\"\"\"\n    import pytest\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp\").exists()')\n    # When no external functions and no os, run() takes the fast path\n    # and OS calls raise NotImplementedError inside Monty\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run()\n    assert str(exc_info.value) == snapshot(\n        \"NotImplementedError: OS function 'Path.exists' not implemented with standard execution\"\n    )\n\n\ndef test_os_not_provided_error_ext_func():\n    \"\"\"Error is raised when OS call is made without os.\"\"\"\n    import pytest\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp\").exists()')\n    # When no external functions and no os, run() takes the fast path\n    # and OS calls raise NotImplementedError inside Monty\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(external_functions={'x': int})\n    assert str(exc_info.value) == snapshot(\"NotImplementedError: OS function 'Path.exists' not implemented\")\n\n\ndef test_not_callable():\n    \"\"\"Raise NotImplementedError inside inside monty if so os\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/test.txt\").exists()')\n\n    with pytest.raises(TypeError, match=\"TypeError: 'int' object is not callable\"):\n        m.run(os=123)  # type: ignore\n\n\n# =============================================================================\n# os.getenv() tests\n# =============================================================================\n\n\ndef test_os_getenv_yields_oscall():\n    \"\"\"os.getenv() yields an OS call with correct function and args.\"\"\"\n    m = pydantic_monty.Monty('import os; os.getenv(\"HOME\")')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('os.getenv')\n    assert result.args == snapshot(('HOME', None))\n\n\ndef test_os_getenv_with_default_yields_oscall():\n    \"\"\"os.getenv() with default yields an OS call with both args.\"\"\"\n    m = pydantic_monty.Monty('import os; os.getenv(\"MISSING\", \"fallback\")')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('os.getenv')\n    assert result.args == snapshot(('MISSING', 'fallback'))\n\n\ndef test_os_getenv_callback():\n    \"\"\"os.getenv() with os works correctly.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> str | None:\n        if function_name == 'os.getenv':\n            key, default = args\n            env = {'HOME': '/home/user', 'USER': 'testuser'}\n            return env.get(key, default)\n        return None\n\n    m = pydantic_monty.Monty('import os; os.getenv(\"HOME\")')\n    result = m.run(os=os_handler)\n    assert result == snapshot('/home/user')\n\n\ndef test_os_getenv_callback_missing():\n    \"\"\"os.getenv() returns None for missing env var when no default.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> str | None:\n        if function_name == 'os.getenv':\n            key, default = args\n            env: dict[str, str] = {}\n            return env.get(key, default)\n        return None\n\n    m = pydantic_monty.Monty('import os; os.getenv(\"NONEXISTENT\")')\n    result = m.run(os=os_handler)\n    assert result is None\n\n\ndef test_os_getenv_callback_with_default():\n    \"\"\"os.getenv() uses default when env var is missing.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> str | None:\n        if function_name == 'os.getenv':\n            key, default = args\n            env: dict[str, str] = {}\n            return env.get(key, default)\n        return None\n\n    m = pydantic_monty.Monty('import os; os.getenv(\"NONEXISTENT\", \"default_value\")')\n    result = m.run(os=os_handler)\n    assert result == snapshot('default_value')\n\n\n# =============================================================================\n# os.environ tests\n# =============================================================================\n\n\ndef test_os_environ_yields_oscall():\n    \"\"\"os.environ yields an OS call with correct function name.\"\"\"\n    m = pydantic_monty.Monty('import os; os.environ')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('os.environ')\n    assert result.args == snapshot(())\n\n\ndef test_os_environ_key_access():\n    \"\"\"os.environ['KEY'] works correctly after getting environ dict.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {'HOME': '/home/user', 'USER': 'testuser'}\n        return None\n\n    m = pydantic_monty.Monty(\"import os; os.environ['HOME']\")\n    result = m.run(os=os_handler)\n    assert result == snapshot('/home/user')\n\n\ndef test_os_environ_key_missing_raises():\n    \"\"\"os.environ['MISSING'] raises KeyError.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {}\n        return None\n\n    m = pydantic_monty.Monty(\"import os; os.environ['MISSING']\")\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(os=os_handler)\n    assert str(exc_info.value) == snapshot('KeyError: MISSING')\n\n\ndef test_os_environ_get_method():\n    \"\"\"os.environ.get() works correctly.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {'HOME': '/home/user'}\n        return None\n\n    m = pydantic_monty.Monty(\"import os; os.environ.get('HOME')\")\n    result = m.run(os=os_handler)\n    assert result == snapshot('/home/user')\n\n\ndef test_os_environ_get_with_default():\n    \"\"\"os.environ.get() with default for missing key.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {}\n        return None\n\n    m = pydantic_monty.Monty(\"import os; os.environ.get('MISSING', 'default')\")\n    result = m.run(os=os_handler)\n    assert result == snapshot('default')\n\n\ndef test_os_environ_len():\n    \"\"\"len(os.environ) returns correct count.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {'A': '1', 'B': '2', 'C': '3'}\n        return None\n\n    m = pydantic_monty.Monty('import os; len(os.environ)')\n    result = m.run(os=os_handler)\n    assert result == snapshot(3)\n\n\ndef test_os_environ_contains():\n    \"\"\"'KEY' in os.environ works correctly.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {'HOME': '/home/user'}\n        return None\n\n    m = pydantic_monty.Monty(\"import os; ('HOME' in os.environ, 'MISSING' in os.environ)\")\n    result = m.run(os=os_handler)\n    assert result == snapshot((True, False))\n\n\ndef test_os_environ_keys():\n    \"\"\"os.environ.keys() returns keys.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {'HOME': '/home', 'USER': 'test'}\n        return None\n\n    m = pydantic_monty.Monty('import os; list(os.environ.keys())')\n    result = m.run(os=os_handler)\n    assert set(result) == snapshot({'HOME', 'USER'})\n\n\ndef test_os_environ_values():\n    \"\"\"os.environ.values() returns values.\"\"\"\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        if function_name == 'os.environ':\n            return {'A': '1', 'B': '2'}\n        return None\n\n    m = pydantic_monty.Monty('import os; list(os.environ.values())')\n    result = m.run(os=os_handler)\n    assert set(result) == snapshot({'1', '2'})\n\n\n# =============================================================================\n# Path write operations - write_text()\n# =============================================================================\n\n\ndef test_path_write_text_yields_oscall():\n    \"\"\"Path.write_text() yields an OS call with correct function, path, and content.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/output.txt\").write_text(\"hello world\")')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.write_text')\n    assert result.args == snapshot((PurePosixPath('/tmp/output.txt'), 'hello world'))\n\n\ndef test_path_write_text_resume():\n    \"\"\"Resuming write_text() with byte count returns it to Monty code.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/output.txt\").write_text(\"hello\")')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=5)  # write_text returns number of bytes written\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(5)\n\n\ndef test_path_write_text_callback():\n    \"\"\"Path.write_text() with os callback works correctly.\"\"\"\n    written_files: dict[str, str] = {}\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> int | None:\n        if function_name == 'Path.write_text':\n            path, content = args\n            written_files[str(path)] = content\n            return len(content.encode('utf-8'))\n        return None\n\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/test.txt\").write_text(\"test content\")')\n    result = m.run(os=os_handler)\n\n    assert result == snapshot(12)\n    assert written_files == snapshot({'/tmp/test.txt': 'test content'})\n\n\n# =============================================================================\n# Path write operations - write_bytes()\n# =============================================================================\n\n\ndef test_path_write_bytes_yields_oscall():\n    \"\"\"Path.write_bytes() yields an OS call with correct function, path, and bytes.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/data.bin\").write_bytes(b\"\\\\x00\\\\x01\\\\x02\")')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.write_bytes')\n    assert result.args == snapshot((PurePosixPath('/tmp/data.bin'), b'\\x00\\x01\\x02'))\n\n\ndef test_path_write_bytes_resume():\n    \"\"\"Resuming write_bytes() with byte count returns it to Monty code.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/data.bin\").write_bytes(b\"abc\")')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=3)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(3)\n\n\n# =============================================================================\n# Path write operations - mkdir()\n# =============================================================================\n\n\ndef test_path_mkdir_yields_oscall():\n    \"\"\"Path.mkdir() yields an OS call with correct function and path.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/newdir\").mkdir()')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.mkdir')\n    assert result.args == snapshot((PurePosixPath('/tmp/newdir'),))\n\n\ndef test_path_mkdir_with_parents_yields_oscall():\n    \"\"\"Path.mkdir(parents=True) yields an OS call with kwargs.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/a/b/c\").mkdir(parents=True)')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.mkdir')\n    assert result.args == snapshot((PurePosixPath('/tmp/a/b/c'),))\n    assert result.kwargs == snapshot({'parents': True})\n\n\ndef test_path_mkdir_with_exist_ok_yields_oscall():\n    \"\"\"Path.mkdir(exist_ok=True) yields an OS call with kwargs.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/existing\").mkdir(exist_ok=True)')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.mkdir')\n    assert result.kwargs == snapshot({'exist_ok': True})\n\n\ndef test_path_mkdir_with_both_kwargs():\n    \"\"\"Path.mkdir(parents=True, exist_ok=True) yields an OS call with both kwargs.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/a/b\").mkdir(parents=True, exist_ok=True)')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.kwargs == snapshot({'parents': True, 'exist_ok': True})\n\n\ndef test_path_mkdir_resume():\n    \"\"\"Resuming mkdir() with None returns correctly.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/newdir\").mkdir()')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=None)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output is None\n\n\n# =============================================================================\n# Path write operations - unlink()\n# =============================================================================\n\n\ndef test_path_unlink_yields_oscall():\n    \"\"\"Path.unlink() yields an OS call with correct function and path.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/to_delete.txt\").unlink()')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.unlink')\n    assert result.args == snapshot((PurePosixPath('/tmp/to_delete.txt'),))\n\n\ndef test_path_unlink_resume():\n    \"\"\"Resuming unlink() with None returns correctly.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/file.txt\").unlink()')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=None)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output is None\n\n\n# =============================================================================\n# Path write operations - rmdir()\n# =============================================================================\n\n\ndef test_path_rmdir_yields_oscall():\n    \"\"\"Path.rmdir() yields an OS call with correct function and path.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/empty_dir\").rmdir()')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.rmdir')\n    assert result.args == snapshot((PurePosixPath('/tmp/empty_dir'),))\n\n\ndef test_path_rmdir_resume():\n    \"\"\"Resuming rmdir() with None returns correctly.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/dir\").rmdir()')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    result = snapshot_result.resume(return_value=None)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output is None\n\n\n# =============================================================================\n# Path write operations - rename()\n# =============================================================================\n\n\ndef test_path_rename_yields_oscall():\n    \"\"\"Path.rename() yields an OS call with source and target paths.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/old.txt\").rename(Path(\"/tmp/new.txt\"))')\n    result = m.start()\n\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.is_os_function is True\n    assert result.function_name == snapshot('Path.rename')\n    assert result.args == snapshot((PurePosixPath('/tmp/old.txt'), PurePosixPath('/tmp/new.txt')))\n\n\ndef test_path_rename_resume():\n    \"\"\"Resuming rename() returns the new path.\"\"\"\n    m = pydantic_monty.Monty('from pathlib import Path; Path(\"/tmp/old.txt\").rename(Path(\"/tmp/new.txt\"))')\n    snapshot_result = m.start()\n\n    assert isinstance(snapshot_result, pydantic_monty.FunctionSnapshot)\n    # rename() returns None (the new Path is constructed by Monty)\n    result = snapshot_result.resume(return_value=None)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output is None\n\n\n# =============================================================================\n# Write operations with os callback\n# =============================================================================\n\n\ndef test_write_operations_callback():\n    \"\"\"Multiple write operations work with os callback.\"\"\"\n    operations: list[tuple[str, tuple[Any, ...]]] = []\n\n    def os_handler(function_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Any:\n        operations.append((function_name, args))\n        match function_name:\n            case 'Path.mkdir':\n                return None\n            case 'Path.write_text':\n                return len(args[1].encode('utf-8'))\n            case 'Path.exists':\n                return True\n            case 'Path.read_text':\n                return 'file content'\n            case _:\n                return None\n\n    code = \"\"\"\nfrom pathlib import Path\nPath('/tmp/mydir').mkdir()\nPath('/tmp/mydir/file.txt').write_text('hello')\nPath('/tmp/mydir/file.txt').read_text()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    result = m.run(os=os_handler)\n\n    assert result == snapshot('file content')\n    assert operations == snapshot(\n        [\n            ('Path.mkdir', (PurePosixPath('/tmp/mydir'),)),\n            ('Path.write_text', (PurePosixPath('/tmp/mydir/file.txt'), 'hello')),\n            ('Path.read_text', (PurePosixPath('/tmp/mydir/file.txt'),)),\n        ]\n    )\n"
  },
  {
    "path": "crates/monty-python/tests/test_print.py",
    "content": "from typing import Callable, Literal\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\nPrintCallback = Callable[[Literal['stdout'], str], None]\n\n\ndef make_print_collector() -> tuple[list[str], PrintCallback]:\n    \"\"\"Create a print callback that collects output into a list.\"\"\"\n    output: list[str] = []\n\n    def callback(stream: Literal['stdout'], text: str) -> None:\n        assert stream == 'stdout'\n        output.append(text)\n\n    return output, callback\n\n\ndef test_print_basic() -> None:\n    m = pydantic_monty.Monty('print(\"hello\")')\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('hello\\n')\n\n\ndef test_print_multiple() -> None:\n    code = \"\"\"\nprint(\"line 1\")\nprint(\"line 2\")\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('line 1\\nline 2\\n')\n\n\ndef test_print_with_values() -> None:\n    m = pydantic_monty.Monty('print(1, 2, 3)')\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('1 2 3\\n')\n\n\ndef test_print_with_sep() -> None:\n    m = pydantic_monty.Monty('print(1, 2, 3, sep=\"-\")')\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('1-2-3\\n')\n\n\ndef test_print_with_end() -> None:\n    m = pydantic_monty.Monty('print(\"hello\", end=\"!\")')\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('hello!')\n\n\ndef test_print_returns_none() -> None:\n    m = pydantic_monty.Monty('print(\"test\")')\n    _, callback = make_print_collector()\n    result = m.run(print_callback=callback)\n    assert result is None\n\n\ndef test_print_empty() -> None:\n    m = pydantic_monty.Monty('print()')\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('\\n')\n\n\ndef test_print_with_limits() -> None:\n    \"\"\"Verify print_callback works together with resource limits.\"\"\"\n    m = pydantic_monty.Monty('print(\"with limits\")')\n    output, callback = make_print_collector()\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=5.0)\n    m.run(print_callback=callback, limits=limits)\n    assert ''.join(output) == snapshot('with limits\\n')\n\n\ndef test_print_with_inputs() -> None:\n    \"\"\"Verify print_callback works together with inputs.\"\"\"\n    m = pydantic_monty.Monty('print(x)', inputs=['x'])\n    output, callback = make_print_collector()\n    m.run(inputs={'x': 42}, print_callback=callback)\n    assert ''.join(output) == snapshot('42\\n')\n\n\ndef test_print_in_loop() -> None:\n    code = \"\"\"\nfor i in range(3):\n    print(i)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('0\\n1\\n2\\n')\n\n\ndef test_print_mixed_types() -> None:\n    m = pydantic_monty.Monty('print(1, \"hello\", True, None)')\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('1 hello True None\\n')\n\n\ndef make_error_callback(error: Exception) -> PrintCallback:\n    \"\"\"Create a print callback that raises an exception.\"\"\"\n\n    def callback(stream: Literal['stdout'], text: str) -> None:\n        raise error\n\n    return callback\n\n\ndef test_print_callback_raises_value_error() -> None:\n    \"\"\"Test that ValueError raised in callback propagates correctly.\"\"\"\n    m = pydantic_monty.Monty('print(\"hello\")')\n    callback = make_error_callback(ValueError('callback error'))\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(print_callback=callback)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('callback error')\n\n\ndef test_print_callback_raises_type_error() -> None:\n    \"\"\"Test that TypeError raised in callback propagates correctly.\"\"\"\n    m = pydantic_monty.Monty('print(\"hello\")')\n    callback = make_error_callback(TypeError('wrong type'))\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(print_callback=callback)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, TypeError)\n    assert inner.args[0] == snapshot('wrong type')\n\n\ndef test_print_callback_raises_in_function() -> None:\n    \"\"\"Test exception from callback when print is called inside a function.\"\"\"\n    code = \"\"\"\ndef greet(name):\n    print(f\"Hello, {name}!\")\n\ngreet(\"World\")\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    callback = make_error_callback(RuntimeError('io error'))\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(print_callback=callback)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, RuntimeError)\n    assert inner.args[0] == snapshot('io error')\n\n\ndef test_print_callback_raises_in_nested_function() -> None:\n    \"\"\"Test exception from callback when print is called in nested functions.\"\"\"\n    code = \"\"\"\ndef outer():\n    def inner():\n        print(\"from inner\")\n    inner()\n\nouter()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    callback = make_error_callback(ValueError('nested error'))\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(print_callback=callback)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('nested error')\n\n\ndef test_print_callback_raises_in_loop() -> None:\n    \"\"\"Test exception from callback when print is called in a loop.\"\"\"\n    code = \"\"\"\nfor i in range(5):\n    print(i)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    call_count = 0\n\n    def callback(stream: Literal['stdout'], text: str) -> None:\n        nonlocal call_count\n        call_count += 1\n        if call_count >= 3:\n            raise ValueError('stopped at 3')\n\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(print_callback=callback)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('stopped at 3')\n    assert call_count == snapshot(3)\n\n\ndef test_map_print() -> None:\n    \"\"\"Test that print can be used inside map.\"\"\"\n    code = \"\"\"\nlist(map(print, [1, 2, 3]))\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output, callback = make_print_collector()\n    m.run(print_callback=callback)\n    assert ''.join(output) == snapshot('1\\n2\\n3\\n')\n"
  },
  {
    "path": "crates/monty-python/tests/test_re.py",
    "content": "import re\nimport sys\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_re_module():\n    m = pydantic_monty.Monty('import re')\n    output = m.run()\n    assert output is None\n\n\ndef test_re_compile():\n    code = \"\"\"\nimport re\npattern = re.compile(r'\\\\d+')\nmatches = pattern.findall('There are 24 hours in a day and 365 days in a year.')\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    assert output is None\n\n\nsupported_flags = [\n    (['re.I', 're.IGNORECASE'], re.IGNORECASE),\n    (['re.M', 're.MULTILINE'], re.MULTILINE),\n    (['re.S', 're.DOTALL'], re.DOTALL),\n]\nif sys.version_info >= (3, 11):\n    supported_flags.append((['re.NOFLAG'], re.NOFLAG))\n\n\n@pytest.mark.parametrize(\n    'flags,target',\n    supported_flags,\n    ids=str,\n)\ndef test_re_constant(flags: list[str], target: int):\n    code = f'import re; ({\",\".join(flags)},)'\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    assert all(map(lambda orig: orig == target, output))\n\n\ndef test_re_compile_repr():\n    code = r\"\"\"\nimport re\npattern = re.compile(r'\\d+', re.IGNORECASE | re.DOTALL)\npattern\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    assert output == r\"re.compile('\\\\d+', re.IGNORECASE|re.DOTALL)\"\n\n\ndef test_re_match_repr():\n    code = \"\"\"\nimport re\npattern = re.compile(r'\\\\d+')\npattern.match('123abc')\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    assert output == \"<re.Match object; span=(0, 3), match='123'>\"\n\n\ndef test_re_match_groups():\n    code = \"\"\"\nimport re\npattern = re.compile(r'(\\\\d+)-(\\\\w+)')\nmatch = pattern.match('123-abc')\nmatch.groups()\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    assert output == ('123', 'abc')\n\n\ndef test_re_substitution():\n    code = \"\"\"\nimport re\npattern = re.compile(r'\\\\s+')\nresult = pattern.sub('-', 'This is a test.')\nresult\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    assert output == 'This-is-a-test.'\n\n\ndef test_re_error_handling():\n    code = \"\"\"\nimport re\ntry:\n    pattern = re.compile(r'[')\nexcept Exception as e:\n    error_message = str(e)\nerror_message\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    output = m.run()\n    error = 'Parsing error at position 1: Invalid character class'\n    assert error in output\n\n\ndef test_re_resume():\n    code = \"\"\"\nimport re\npattern = re.compile(func())\nmatches = pattern.findall('Sample 123 text 456')\ndump(matches)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    assert progress.function_name == snapshot('func')\n    assert progress.args == snapshot(())\n    assert progress.kwargs == snapshot({})\n\n    progress2 = progress.resume(return_value='\\\\d+')\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    result = progress2.resume(return_value=['123', '456'])\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(['123', '456'])\n\n\ndef test_re_persistence():\n    code = \"\"\"\nimport re\npattern = re.compile(r'\\\\w+')\ndump()\nmatches = pattern.findall('Test 123!')\nmatches\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    result = progress2.resume(return_value=None)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(['Test', '123'])\n\n\ndef test_re_error_upcast():\n    code = \"\"\"\nimport re\nre.compile(r'[')\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    try:\n        m.run()\n        assert False, 'Expected an exception to be raised'\n    except pydantic_monty.MontyRuntimeError as e:\n        error_message = str(e)\n        assert True, 'Expected an exception to be raised'\n        if sys.version_info >= (3, 13):\n            assert type(e.exception()) is re.PatternError\n        else:\n            assert type(e.exception()) is re.error\n        assert 'Parsing error at position 1: Invalid character class' in error_message\n"
  },
  {
    "path": "crates/monty-python/tests/test_readme_examples.py",
    "content": "import pytest\nfrom pytest_examples import CodeExample, EvalExample, find_examples\n\npaths = (\n    'crates/monty-python/README.md',\n    'README.md',\n)\n\n\n@pytest.mark.parametrize('example', find_examples(*paths), ids=str)\ndef test_readme_examples(example: CodeExample, eval_example: EvalExample):\n    eval_example.set_config(target_version='py310', ruff_ignore=['FA102'])\n    eval_example.lint(example)\n    opt_test = example.prefix_settings().get('test', '')\n    if opt_test.startswith('skip'):\n        pytest.skip(opt_test[4:].lstrip(' -') or 'running code skipped')\n    if eval_example.update_examples:\n        eval_example.run_print_update(example)\n    else:\n        eval_example.run_print_check(example)\n"
  },
  {
    "path": "crates/monty-python/tests/test_repl.py",
    "content": "from typing import Callable, Literal\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\nPrintCallback = Callable[[Literal['stdout'], str], None]\n\n\ndef make_print_collector() -> tuple[list[str], PrintCallback]:\n    \"\"\"Create a print callback that collects output into a list.\"\"\"\n    output: list[str] = []\n\n    def callback(stream: Literal['stdout'], text: str) -> None:\n        assert stream == 'stdout'\n        output.append(text)\n\n    return output, callback\n\n\n# === Construction ===\n\n\ndef test_default_construction():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.script_name == snapshot('main.py')\n\n\ndef test_custom_script_name():\n    repl = pydantic_monty.MontyRepl(script_name='test.py')\n    assert repl.script_name == snapshot('test.py')\n\n\ndef test_repr():\n    repl = pydantic_monty.MontyRepl(script_name='my_repl.py')\n    assert repr(repl) == snapshot(\"MontyRepl(script_name='my_repl.py')\")\n\n\n# === Basic feed_run behavior ===\n\n\ndef test_feed_run_expression_returns_value():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('1 + 2') == snapshot(3)\n\n\ndef test_feed_run_assignment_returns_none():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('x = 42') == snapshot(None)\n\n\ndef test_feed_run_empty_string_returns_none():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('') == snapshot(None)\n\n\ndef test_feed_run_none_literal():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('None') is None\n\n\n# === State persistence across feeds ===\n\n\ndef test_variable_persists_across_feeds():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 10')\n    assert repl.feed_run('x') == snapshot(10)\n\n\ndef test_incremental_mutation():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('counter = 0')\n    repl.feed_run('counter = counter + 1')\n    repl.feed_run('counter = counter + 1')\n    assert repl.feed_run('counter') == snapshot(2)\n\n\ndef test_multiple_variables():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 10')\n    repl.feed_run('y = 20')\n    assert repl.feed_run('x + y') == snapshot(30)\n\n\ndef test_function_defined_then_called():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('def double(n):\\n    return n * 2')\n    assert repl.feed_run('double(21)') == snapshot(42)\n\n\ndef test_function_uses_previously_defined_variable():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('factor = 3')\n    repl.feed_run('def multiply(n):\\n    return n * factor')\n    assert repl.feed_run('multiply(7)') == snapshot(21)\n\n\ndef test_list_mutation_persists():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('items = [1, 2, 3]')\n    repl.feed_run('items.append(4)')\n    assert repl.feed_run('len(items)') == snapshot(4)\n    assert repl.feed_run('items') == snapshot([1, 2, 3, 4])\n\n\ndef test_dict_mutation_persists():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run(\"data = {'a': 1}\")\n    repl.feed_run(\"data['b'] = 2\")\n    assert repl.feed_run('len(data)') == snapshot(2)\n    assert repl.feed_run(\"data['b']\") == snapshot(2)\n\n\ndef test_variable_reassignment():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = \"hello\"')\n    assert repl.feed_run('x') == snapshot('hello')\n    repl.feed_run('x = 42')\n    assert repl.feed_run('x') == snapshot(42)\n\n\n# === Multi-statement snippets ===\n\n\ndef test_multi_statement_snippet():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('a = 1\\nb = 2\\nc = a + b')\n    assert repl.feed_run('c') == snapshot(3)\n\n\ndef test_loop_in_snippet():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('total = 0\\nfor i in range(5):\\n    total = total + i')\n    assert repl.feed_run('total') == snapshot(10)\n\n\ndef test_if_else_in_snippet():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 10')\n    repl.feed_run('result = \"big\" if x > 5 else \"small\"')\n    assert repl.feed_run('result') == snapshot('big')\n\n\n# === Return value types ===\n\n\n@pytest.mark.parametrize(\n    'code,expected',\n    [\n        ('42', 42),\n        ('3.14', 3.14),\n        ('\"hello\"', 'hello'),\n        ('True', True),\n        ('False', False),\n        ('[1, 2, 3]', [1, 2, 3]),\n        ('(1, 2, 3)', (1, 2, 3)),\n        (\"{'a': 1}\", {'a': 1}),\n    ],\n    ids=['int', 'float', 'str', 'true', 'false', 'list', 'tuple', 'dict'],\n)\ndef test_feed_run_return_types(code: str, expected: object):\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run(code) == expected\n\n\n# === Error handling ===\n\n\ndef test_syntax_error():\n    repl = pydantic_monty.MontyRepl()\n    with pytest.raises(pydantic_monty.MontySyntaxError):\n        repl.feed_run('def')\n\n\ndef test_runtime_error_preserves_state():\n    \"\"\"A runtime error should not destroy previously defined state.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 42')\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        repl.feed_run('1 / 0')\n    # x should still be accessible after the error\n    assert repl.feed_run('x') == snapshot(42)\n\n\ndef test_name_error():\n    repl = pydantic_monty.MontyRepl()\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run('undefined_var')\n    inner = exc_info.value.exception()\n    assert isinstance(inner, NameError)\n\n\ndef test_type_error():\n    repl = pydantic_monty.MontyRepl()\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run('\"hello\" + 1')\n    inner = exc_info.value.exception()\n    assert isinstance(inner, TypeError)\n\n\ndef test_zero_division_error():\n    repl = pydantic_monty.MontyRepl()\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run('1 / 0')\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ZeroDivisionError)\n\n\ndef test_index_error():\n    repl = pydantic_monty.MontyRepl()\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run('[1, 2][10]')\n    inner = exc_info.value.exception()\n    assert isinstance(inner, IndexError)\n\n\ndef test_key_error():\n    repl = pydantic_monty.MontyRepl()\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run(\"{'a': 1}['b']\")\n    inner = exc_info.value.exception()\n    assert isinstance(inner, KeyError)\n\n\ndef test_multiple_errors_dont_corrupt_state():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 1')\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        repl.feed_run('1 / 0')\n    repl.feed_run('x = x + 1')\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        repl.feed_run('undefined_name')\n    assert repl.feed_run('x') == snapshot(2)\n\n\n# === Print callback ===\n\n\ndef test_print_callback_on_feed():\n    repl = pydantic_monty.MontyRepl()\n    output, callback = make_print_collector()\n    repl.feed_run('print(\"hello\")', print_callback=callback)\n    assert ''.join(output) == snapshot('hello\\n')\n\n\ndef test_print_callback_across_feeds():\n    repl = pydantic_monty.MontyRepl()\n    output, callback = make_print_collector()\n    repl.feed_run('print(\"first\")', print_callback=callback)\n    repl.feed_run('print(\"second\")', print_callback=callback)\n    assert ''.join(output) == snapshot('first\\nsecond\\n')\n\n\n# === Resource limits ===\n\n\ndef test_construction_with_limits():\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=5.0)\n    repl = pydantic_monty.MontyRepl(limits=limits)\n    assert repl.feed_run('1 + 1') == snapshot(2)\n\n\ndef test_infinite_loop_with_limits():\n    limits = pydantic_monty.ResourceLimits(max_duration_secs=0.5)\n    repl = pydantic_monty.MontyRepl(limits=limits)\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        repl.feed_run('while True:\\n    pass')\n\n\n# === Serialization ===\n\n\ndef test_dump_load_roundtrip():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 40')\n    repl.feed_run('x = x + 1')\n\n    serialized = repl.dump()\n    assert isinstance(serialized, bytes)\n\n    loaded = pydantic_monty.MontyRepl.load(serialized)\n    assert loaded.feed_run('x + 1') == snapshot(42)\n\n\ndef test_dump_load_preserves_functions():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('def greet(name):\\n    return \"hello \" + name')\n\n    loaded = pydantic_monty.MontyRepl.load(repl.dump())\n    assert loaded.feed_run('greet(\"world\")') == snapshot('hello world')\n\n\ndef test_dump_load_preserves_script_name():\n    repl = pydantic_monty.MontyRepl(script_name='custom.py')\n    loaded = pydantic_monty.MontyRepl.load(repl.dump())\n    assert loaded.script_name == snapshot('custom.py')\n\n\ndef test_load_with_print_callback():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 1')\n\n    output, callback = make_print_collector()\n    loaded = pydantic_monty.MontyRepl.load(repl.dump())\n    loaded.feed_run('print(x)', print_callback=callback)\n    assert ''.join(output) == snapshot('1\\n')\n\n\ndef test_load_invalid_data():\n    with pytest.raises(ValueError):\n        pydantic_monty.MontyRepl.load(b'invalid data')\n\n\n# === External functions ===\n\n\ndef test_external_function_basic():\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('result = add(3, 4)', external_functions={'add': add}) == snapshot(None)\n    assert repl.feed_run('result') == snapshot(7)\n\n\ndef test_external_function_return_value():\n    def greet(name: str) -> str:\n        return f'hello {name}'\n\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('greet(\"world\")', external_functions={'greet': greet}) == snapshot('hello world')\n\n\ndef test_external_function_called_multiple_times():\n    call_count = 0\n\n    def counter():\n        nonlocal call_count\n        call_count += 1\n        return call_count\n\n    repl = pydantic_monty.MontyRepl()\n    ext = {'counter': counter}\n    assert repl.feed_run('counter()', external_functions=ext) == snapshot(1)\n    assert repl.feed_run('counter()', external_functions=ext) == snapshot(2)\n    assert call_count == 2\n\n\ndef test_external_function_persists_state_across_feeds():\n    def double(x: int) -> int:\n        return x * 2\n\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 5')\n    assert repl.feed_run('double(x)', external_functions={'double': double}) == snapshot(10)\n\n\ndef test_external_function_exception_becomes_runtime_error():\n    def fail():\n        raise ValueError('external failure')\n\n    repl = pydantic_monty.MontyRepl()\n    ext = {'fail': fail}\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run('fail()', external_functions=ext)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert str(inner) == snapshot('external failure')\n\n\ndef test_external_function_error_preserves_repl_state():\n    def fail():\n        raise ValueError('boom')\n\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 42')\n    ext = {'fail': fail}\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        repl.feed_run('fail()', external_functions=ext)\n    # REPL state should be preserved after error\n    assert repl.feed_run('x') == snapshot(42)\n\n\ndef test_external_function_undefined_raises_name_error():\n    \"\"\"Calling a name that's not in external_functions raises NameError.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    ext = {'known': lambda: 1}\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        repl.feed_run('unknown()', external_functions=ext)\n    inner = exc_info.value.exception()\n    assert isinstance(inner, NameError)\n\n\ndef test_external_function_with_print_callback():\n    output, callback = make_print_collector()\n    repl = pydantic_monty.MontyRepl()\n    ext = {'get_msg': lambda: 'from external'}\n    repl.feed_run('x = get_msg()\\nprint(x)', external_functions=ext, print_callback=callback)\n    assert ''.join(output) == snapshot('from external\\n')\n\n\ndef test_external_function_with_kwargs():\n    def greet(name: str, greeting: str = 'hello') -> str:\n        return f'{greeting} {name}'\n\n    repl = pydantic_monty.MontyRepl()\n    ext = {'greet': greet}\n    assert repl.feed_run(\"greet('world', greeting='hi')\", external_functions=ext) == snapshot('hi world')\n\n\ndef test_feed_run_no_externals_with_os_preserves_repl_state():\n    \"\"\"feed_run with os= but no external_functions= preserves REPL state when an external call is hit.\n\n    When os= is provided, feed_run uses the feed_start_loop path. If a non-OS external\n    function is called but external_functions was not provided, the loop must restore\n    the REPL before returning the error.\n    \"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 42')\n\n    # Provide os= to force the feed_start_loop path, but no external_functions\n    def dummy_os(func: str, args: object, kwargs: object) -> None:\n        pass\n\n    with pytest.raises(RuntimeError, match='no external_functions provided'):\n        repl.feed_run('unknown_func()', os=dummy_os)\n    # REPL state must be preserved — previously this was lost\n    assert repl.feed_run('x') == snapshot(42)\n\n\n# === Inputs ===\n\n\ndef test_inputs_basic():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('x + 1', inputs={'x': 10}) == snapshot(11)\n\n\ndef test_inputs_used_in_same_snippet():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('y = x + 1', inputs={'x': 42})\n    assert repl.feed_run('y') == snapshot(43)\n\n\ndef test_inputs_multiple_values():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('a + b', inputs={'a': 3, 'b': 7}) == snapshot(10)\n\n\ndef test_inputs_override_existing_variable():\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 1')\n    assert repl.feed_run('x', inputs={'x': 99}) == snapshot(99)\n\n\ndef test_inputs_with_external_functions():\n    def double(n: int) -> int:\n        return n * 2\n\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('double(x)', inputs={'x': 5}, external_functions={'double': double}) == snapshot(10)\n\n\n# === Tests for MontyRepl.feed_start() ===\n\n\ndef test_feed_start_no_external_calls():\n    \"\"\"feed_start with no external calls returns MontyComplete directly.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('1 + 2')\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(3)\n    # REPL should still be usable\n    assert repl.feed_run('3 + 4') == snapshot(7)\n\n\ndef test_feed_start_state_persists():\n    \"\"\"feed_start preserves REPL state from prior feed_run calls.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 10')\n    progress = repl.feed_start('x + 5')\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(15)\n\n\ndef test_feed_start_external_function():\n    \"\"\"feed_start yields FunctionSnapshot for external function calls.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('add(1, 2)')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('add')\n    assert progress.args == snapshot((1, 2))\n    progress = progress.resume(return_value=3)\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(3)\n    # REPL should still be usable after\n    assert repl.feed_run('1 + 1') == snapshot(2)\n\n\ndef test_feed_start_external_function_preserves_state():\n    \"\"\"feed_start async result is accessible in subsequent feed_run calls.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('result = add(1, 2)')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    progress = progress.resume(return_value=42)\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert repl.feed_run('result') == snapshot(42)\n\n\ndef test_feed_start_multiple_external_calls():\n    \"\"\"feed_start handles multiple sequential external calls.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    code = 'a = foo(1)\\nb = bar(2)\\na + b'\n    progress = repl.feed_start(code)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('foo')\n    progress = progress.resume(return_value=10)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('bar')\n    progress = progress.resume(return_value=20)\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(30)\n\n\ndef test_feed_start_error_preserves_repl_state():\n    \"\"\"REPL state is preserved when feed_start raises an error.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 42')\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        repl.feed_start('1 / 0')\n    # REPL should still be usable\n    assert repl.feed_run('x') == snapshot(42)\n\n\ndef test_feed_start_resume_error_preserves_repl_state():\n    \"\"\"REPL state is preserved when resume raises a runtime error.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 99')\n    progress = repl.feed_start('fail()')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    # Resume with an exception that isn't caught\n    with pytest.raises(pydantic_monty.MontyRuntimeError):\n        progress.resume(exception=ValueError('boom'))\n    assert repl.feed_run('x') == snapshot(99)\n\n\ndef test_feed_start_with_inputs():\n    \"\"\"feed_start supports the inputs parameter.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('process(x)', inputs={'x': 5})\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('process')\n    assert progress.args == snapshot((5,))\n    progress = progress.resume(return_value=25)\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(25)\n\n\ndef test_feed_start_with_print_callback():\n    \"\"\"feed_start supports the print_callback parameter.\"\"\"\n    output: list[tuple[str, str]] = []\n\n    def callback(stream: str, text: str) -> None:\n        output.append((stream, text))\n\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('print(\"hello\")', print_callback=callback)\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert output == snapshot([('stdout', 'hello'), ('stdout', '\\n')])\n\n\ndef test_feed_start_name_lookup():\n    \"\"\"feed_start yields NameLookupSnapshot for bare name access.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('x = foo')\n    assert isinstance(progress, pydantic_monty.NameLookupSnapshot)\n    assert progress.variable_name == snapshot('foo')\n    progress = progress.resume(value=42)\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert repl.feed_run('x') == snapshot(42)\n\n\ndef test_feed_start_dump_load_repl_snapshot():\n    \"\"\"FunctionSnapshot from feed_start can be serialized and deserialized with load_repl_snapshot.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('x = 10')\n    progress = repl.feed_start('add(x, 2)')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    loaded, loaded_repl = pydantic_monty.load_repl_snapshot(data)\n    assert isinstance(loaded, pydantic_monty.FunctionSnapshot)\n    assert loaded.function_name == snapshot('add')\n    assert loaded.args == snapshot((10, 2))\n\n    # Resume the loaded snapshot\n    result = loaded.resume(return_value=12)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(12)\n\n    # REPL state is restored and usable\n    assert loaded_repl.feed_run('x') == snapshot(10)\n\n\ndef test_feed_start_dump_load_repl_snapshot_preserves_state():\n    \"\"\"REPL state from before feed_start is preserved through dump/load.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    repl.feed_run('counter = 0')\n    repl.feed_run('counter = counter + 1')\n    repl.feed_run('counter = counter + 1')\n\n    progress = repl.feed_start('result = fetch(counter)')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.args == snapshot((2,))\n\n    data = progress.dump()\n    loaded, loaded_repl = pydantic_monty.load_repl_snapshot(data)\n    assert isinstance(loaded, pydantic_monty.FunctionSnapshot)\n    result = loaded.resume(return_value='done')\n    assert isinstance(result, pydantic_monty.MontyComplete)\n\n    # Counter should still be 2, and result should be set\n    assert loaded_repl.feed_run('counter') == snapshot(2)\n    assert loaded_repl.feed_run('result') == snapshot('done')\n\n\ndef test_feed_start_dump_load_repl_snapshot_name_lookup():\n    \"\"\"NameLookupSnapshot from feed_start can be serialized and deserialized.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('x = foo')\n    assert isinstance(progress, pydantic_monty.NameLookupSnapshot)\n    assert progress.variable_name == snapshot('foo')\n\n    data = progress.dump()\n    loaded, loaded_repl = pydantic_monty.load_repl_snapshot(data)\n    assert isinstance(loaded, pydantic_monty.NameLookupSnapshot)\n    assert loaded.variable_name == snapshot('foo')\n\n    result = loaded.resume(value=99)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert loaded_repl.feed_run('x') == snapshot(99)\n\n\ndef test_feed_start_dump_load_repl_snapshot_multiple_calls():\n    \"\"\"Multiple external calls with dump/load between each.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('a = foo(1)\\nb = bar(2)\\na + b')\n\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('foo')\n\n    # Dump/load between first and second call\n    data = progress.dump()\n    loaded, _ = pydantic_monty.load_repl_snapshot(data)\n    assert isinstance(loaded, pydantic_monty.FunctionSnapshot)\n    progress2 = loaded.resume(return_value=10)\n\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n    assert progress2.function_name == snapshot('bar')\n\n    # Dump/load between second call and completion\n    data2 = progress2.dump()\n    loaded2, _ = pydantic_monty.load_repl_snapshot(data2)\n    assert isinstance(loaded2, pydantic_monty.FunctionSnapshot)\n    result = loaded2.resume(return_value=20)\n\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(30)\n\n\ndef test_feed_start_dump_load_snapshot_errors_on_repl():\n    \"\"\"load_snapshot rejects REPL snapshots — the wire formats are incompatible.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('fetch(1)')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    # REPL snapshots use a different wire format, so load_snapshot fails on deserialization\n    with pytest.raises(ValueError):\n        pydantic_monty.load_snapshot(data)\n\n\ndef test_feed_start_dump_load_repl_snapshot_with_print_callback():\n    \"\"\"print_callback works on loaded REPL snapshots.\"\"\"\n    output, callback = make_print_collector()\n\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('x = fetch()')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    loaded, loaded_repl = pydantic_monty.load_repl_snapshot(data, print_callback=callback)\n    assert isinstance(loaded, pydantic_monty.FunctionSnapshot)\n    # Resume — the loaded snapshot should use the print callback for subsequent prints\n    loaded.resume(return_value=42)\n    loaded_repl.feed_run('print(x)', print_callback=callback)\n    assert ''.join(output) == snapshot('42\\n')\n\n\ndef test_feed_start_dump_load_repl_snapshot_preserves_script_name():\n    \"\"\"Script name is preserved through REPL snapshot dump/load.\"\"\"\n    repl = pydantic_monty.MontyRepl(script_name='my_repl.py')\n    progress = repl.feed_start('fetch()')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    loaded, loaded_repl = pydantic_monty.load_repl_snapshot(data)\n    assert loaded.script_name == snapshot('my_repl.py')\n    assert loaded_repl.script_name == snapshot('my_repl.py')\n\n\ndef test_non_repl_dump_load_with_load_snapshot():\n    \"\"\"Non-REPL snapshots from Monty.start() work with load_snapshot.\"\"\"\n    m = pydantic_monty.Monty('func(1, 2)')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    loaded = pydantic_monty.load_snapshot(data)\n    assert isinstance(loaded, pydantic_monty.FunctionSnapshot)\n    assert loaded.function_name == snapshot('func')\n    assert loaded.args == snapshot((1, 2))\n\n    result = loaded.resume(return_value=100)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(100)\n\n\ndef test_feed_start_dump_after_resume_fails():\n    \"\"\"Cannot dump a REPL snapshot that has already been resumed.\"\"\"\n    repl = pydantic_monty.MontyRepl()\n    progress = repl.feed_start('fetch()')\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    progress.resume(return_value=1)\n\n    with pytest.raises(RuntimeError) as exc_info:\n        progress.dump()\n    assert exc_info.value.args[0] == snapshot('Cannot dump progress that has already been resumed')\n\n\ndef test_inputs_various_types():\n    repl = pydantic_monty.MontyRepl()\n    assert repl.feed_run('s', inputs={'s': 'hello'}) == snapshot('hello')\n    assert repl.feed_run('n', inputs={'n': 42}) == snapshot(42)\n    assert repl.feed_run('f', inputs={'f': 3.14}) == snapshot(3.14)\n    assert repl.feed_run('b', inputs={'b': True}) == snapshot(True)\n    assert repl.feed_run('lst', inputs={'lst': [1, 2]}) == snapshot([1, 2])\n"
  },
  {
    "path": "crates/monty-python/tests/test_serialize.py",
    "content": "from dataclasses import dataclass, is_dataclass\nfrom typing import Any\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_monty_dump_load_roundtrip():\n    m = pydantic_monty.Monty('x + 1', inputs=['x'])\n    data = m.dump()\n\n    assert isinstance(data, bytes)\n    assert len(data) > 0\n\n    m2 = pydantic_monty.Monty.load(data)\n    assert m2.run(inputs={'x': 41}) == snapshot(42)\n\n\ndef test_monty_dump_load_preserves_script_name():\n    m = pydantic_monty.Monty('1', script_name='custom.py')\n    data = m.dump()\n\n    m2 = pydantic_monty.Monty.load(data)\n    assert repr(m2) == snapshot(\"Monty(<1 line of code>, script_name='custom.py')\")\n\n\ndef test_monty_dump_load_preserves_inputs():\n    m = pydantic_monty.Monty('x + y', inputs=['x', 'y'])\n    data = m.dump()\n\n    m2 = pydantic_monty.Monty.load(data)\n    assert m2.run(inputs={'x': 1, 'y': 2}) == snapshot(3)\n\n\ndef test_monty_dump_load_preserves_external_functions():\n    m = pydantic_monty.Monty('func()')\n    data = m.dump()\n\n    m2 = pydantic_monty.Monty.load(data)\n    result = m2.run(external_functions={'func': lambda: 42})\n    assert result == snapshot(42)\n\n\ndef test_monty_load_invalid_data():\n    with pytest.raises(ValueError) as exc_info:\n        pydantic_monty.Monty.load(b'invalid data')\n    assert str(exc_info.value) == snapshot('Hit the end of buffer, expected more data')\n\n\ndef test_progress_dump_load_roundtrip():\n    m = pydantic_monty.Monty('func(1, 2)')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    assert isinstance(data, bytes)\n    assert len(data) > 0\n\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n    assert progress2.function_name == snapshot('func')\n    assert progress2.args == snapshot((1, 2))\n    assert progress2.kwargs == snapshot({})\n\n    result = progress2.resume(return_value=100)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(100)\n\n\ndef test_progress_dump_load_preserves_script_name():\n    m = pydantic_monty.Monty('func()', script_name='test.py')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n    assert progress2.script_name == snapshot('test.py')\n\n\ndef test_progress_dump_load_with_kwargs():\n    m = pydantic_monty.Monty('func(a=1, b=\"hello\")')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n    assert progress2.function_name == snapshot('func')\n    assert progress2.args == snapshot(())\n    assert progress2.kwargs == snapshot({'a': 1, 'b': 'hello'})\n\n\ndef test_progress_dump_after_resume_fails():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    progress.resume(return_value=1)\n\n    with pytest.raises(RuntimeError) as exc_info:\n        progress.dump()\n    assert exc_info.value.args[0] == snapshot('Cannot dump progress that has already been resumed')\n\n\ndef test_progress_load_invalid_data():\n    with pytest.raises(ValueError):\n        pydantic_monty.load_snapshot(b'invalid data')\n\n\ndef test_progress_dump_load_multiple_calls():\n    m = pydantic_monty.Monty('a() + b()')\n\n    # First call\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('a')\n\n    # Dump and load the state\n    data = progress.dump()\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    # Resume with first return value\n    progress3 = progress2.resume(return_value=10)\n    assert isinstance(progress3, pydantic_monty.FunctionSnapshot)\n    assert progress3.function_name == snapshot('b')\n\n    # Dump and load again\n    data2 = progress3.dump()\n    progress4 = pydantic_monty.load_snapshot(data2)\n    assert isinstance(progress4, pydantic_monty.FunctionSnapshot)\n\n    # Resume with second return value\n    result = progress4.resume(return_value=5)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(15)\n\n\ndef test_progress_load_with_print_callback():\n    output: list[tuple[str, str]] = []\n\n    def callback(stream: str, text: str) -> None:\n        output.append((stream, text))\n\n    m = pydantic_monty.Monty('print(\"before\"); func(); print(\"after\")')\n    progress = m.start(print_callback=callback)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert output == snapshot([('stdout', 'before'), ('stdout', '\\n')])\n\n    # Dump and load with new callback\n    data = progress.dump()\n    output.clear()\n    progress2 = pydantic_monty.load_snapshot(data, print_callback=callback)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    result = progress2.resume(return_value=None)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert output == snapshot([('stdout', 'after'), ('stdout', '\\n')])\n\n\ndef test_progress_load_without_print_callback():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    result = progress2.resume(return_value=42)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(42)\n\n\n@pytest.mark.parametrize(\n    'code,expected',\n    [\n        ('1 + 1', 2),\n        ('\"hello\"', 'hello'),\n        ('[1, 2, 3]', [1, 2, 3]),\n        ('{\"a\": 1}', {'a': 1}),\n        ('True', True),\n        ('None', None),\n    ],\n)\ndef test_monty_dump_load_various_outputs(code: str, expected: Any):\n    m = pydantic_monty.Monty(code)\n    data = m.dump()\n    m2 = pydantic_monty.Monty.load(data)\n    assert m2.run() == expected\n\n\ndef test_progress_dump_load_with_limits():\n    m = pydantic_monty.Monty('func()')\n    limits = pydantic_monty.ResourceLimits(max_allocations=1000)\n    progress = m.start(limits=limits)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    result = progress2.resume(return_value=99)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(99)\n\n\n@dataclass\nclass Person:\n    name: str\n    age: int\n\n\ndef test_monty_load_dataclass():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    data = m.dump()\n\n    m2 = pydantic_monty.Monty.load(data)\n    m2.register_dataclass(Person)\n    result = m2.run(inputs={'x': Person(name='Alice', age=30)})\n    assert isinstance(result, Person)\n\n\ndef test_progress_dump_load_dataclass():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    data = progress.dump()\n    assert isinstance(data, bytes)\n    assert len(data) > 0\n\n    progress2 = pydantic_monty.load_snapshot(data, dataclass_registry=[Person])\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n    assert progress2.function_name == snapshot('func')\n    assert progress2.args == snapshot(())\n    assert progress2.kwargs == snapshot({})\n\n    result = progress2.resume(return_value=Person(name='Alice', age=30))\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert isinstance(result.output, Person)\n    assert result.output.name == snapshot('Alice')\n    assert result.output.age == snapshot(30)\n\n\ndef test_progress_dump_load_unknown_dataclass():\n    \"\"\"When a snapshot containing a dataclass is loaded without registering the type,\n    the result should be an UnknownDataclass with the correct attributes.\"\"\"\n    m = pydantic_monty.Monty(\n        'external_call()\\nx',\n        inputs=['x'],\n    )\n    progress = m.start(inputs={'x': Person(name='Bob', age=25)})\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('external_call')\n\n    # Dump the snapshot (dataclass x is in the heap)\n    data = progress.dump()\n\n    # Load WITHOUT providing dataclass_registry — Person type is unknown\n    progress2 = pydantic_monty.load_snapshot(data)\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n\n    # Resume execution — x is returned as UnknownDataclass\n    result = progress2.resume(return_value=None)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n\n    output = result.output\n    # Should NOT be a Person instance since the type wasn't registered\n    assert not isinstance(output, Person)\n    assert type(output).__name__ == snapshot('UnknownDataclass')\n\n    # Attributes should still be accessible\n    assert output.name == snapshot('Bob')\n    assert output.age == snapshot(25)\n\n    # Should be compatible with dataclasses module\n    assert is_dataclass(output)\n\n    # repr should indicate it's unknown\n    assert repr(output) == snapshot(\"<Unknown Dataclass Person(name='Bob', age=25)>\")\n"
  },
  {
    "path": "crates/monty-python/tests/test_start.py",
    "content": "from typing import Any\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_start_no_external_functions_returns_complete():\n    m = pydantic_monty.Monty('1 + 2')\n    result = m.start()\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(3)\n\n\ndef test_start_with_external_function_returns_progress():\n    m = pydantic_monty.Monty('func()')\n    result = m.start()\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.script_name == snapshot('main.py')\n    assert result.function_name == snapshot('func')\n    assert result.args == snapshot(())\n    assert result.kwargs == snapshot({})\n\n\ndef test_start_custom_script_name():\n    m = pydantic_monty.Monty('func()', script_name='custom.py')\n    result = m.start()\n    assert isinstance(result, pydantic_monty.FunctionSnapshot)\n    assert result.script_name == snapshot('custom.py')\n\n\ndef test_start_progress_resume_returns_complete():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('func')\n    assert progress.args == snapshot(())\n    assert progress.kwargs == snapshot({})\n\n    result = progress.resume(return_value=42)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(42)\n\n\ndef test_start_progress_with_args():\n    m = pydantic_monty.Monty('func(1, 2, 3)')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('func')\n    assert progress.args == snapshot((1, 2, 3))\n    assert progress.kwargs == snapshot({})\n\n\ndef test_start_progress_with_kwargs():\n    m = pydantic_monty.Monty('func(a=1, b=\"two\")')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('func')\n    assert progress.args == snapshot(())\n    assert progress.kwargs == snapshot({'a': 1, 'b': 'two'})\n\n\ndef test_start_progress_with_mixed_args_kwargs():\n    m = pydantic_monty.Monty('func(1, 2, x=\"hello\", y=True)')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('func')\n    assert progress.args == snapshot((1, 2))\n    assert progress.kwargs == snapshot({'x': 'hello', 'y': True})\n\n\ndef test_start_multiple_external_calls():\n    m = pydantic_monty.Monty('a() + b()')\n\n    # First call\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('a')\n\n    # Resume with first return value\n    progress = progress.resume(return_value=10)\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('b')\n\n    # Resume with second return value\n    result = progress.resume(return_value=5)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(15)\n\n\ndef test_start_chain_of_external_calls():\n    m = pydantic_monty.Monty('c() + c() + c()')\n\n    call_count = 0\n    progress = m.start()\n\n    while isinstance(progress, pydantic_monty.FunctionSnapshot | pydantic_monty.FutureSnapshot):\n        assert isinstance(progress, pydantic_monty.FunctionSnapshot), 'Expected FunctionSnapshot'\n        assert progress.function_name == snapshot('c')\n        call_count += 1\n        progress = progress.resume(return_value=call_count)\n\n    assert isinstance(progress, pydantic_monty.MontyComplete)\n    assert progress.output == snapshot(6)  # 1 + 2 + 3\n    assert call_count == snapshot(3)\n\n\ndef test_start_with_inputs():\n    m = pydantic_monty.Monty('process(x)', inputs=['x'])\n    progress = m.start(inputs={'x': 100})\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert progress.function_name == snapshot('process')\n    assert progress.args == snapshot((100,))\n\n\ndef test_start_with_limits():\n    m = pydantic_monty.Monty('1 + 2')\n    limits = pydantic_monty.ResourceLimits(max_allocations=1000)\n    result = m.start(limits=limits)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(3)\n\n\ndef test_start_with_print_callback():\n    output: list[tuple[str, str]] = []\n\n    def callback(stream: str, text: str) -> None:\n        output.append((stream, text))\n\n    m = pydantic_monty.Monty('print(\"hello\")')\n    result = m.start(print_callback=callback)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert output == snapshot([('stdout', 'hello'), ('stdout', '\\n')])\n\n\ndef test_start_resume_cannot_be_called_twice():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    # First resume succeeds\n    progress.resume(return_value=1)\n\n    # Second resume should fail\n    with pytest.raises(RuntimeError) as exc_info:\n        progress.resume(return_value=2)\n    assert exc_info.value.args[0] == snapshot('Progress already resumed')\n\n\ndef test_start_complex_return_value():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    result = progress.resume(return_value={'a': [1, 2, 3], 'b': {'nested': True}})\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot({'a': [1, 2, 3], 'b': {'nested': True}})\n\n\ndef test_start_resume_with_none():\n    m = pydantic_monty.Monty('func()')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    result = progress.resume(return_value=None)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output is None\n\n\ndef test_progress_repr():\n    m = pydantic_monty.Monty('func(1, x=2)')\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    assert repr(progress) == snapshot(\n        \"FunctionSnapshot(script_name='main.py', function_name='func', args=(1,), kwargs={'x': 2})\"\n    )\n\n\ndef test_complete_repr():\n    m = pydantic_monty.Monty('42')\n    result = m.start()\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert repr(result) == snapshot('MontyComplete(output=42)')\n\n\ndef test_start_can_reuse_monty_instance():\n    m = pydantic_monty.Monty('func(x)', inputs=['x'])\n\n    # First run\n    progress1 = m.start(inputs={'x': 1})\n    assert isinstance(progress1, pydantic_monty.FunctionSnapshot)\n    assert progress1.args == snapshot((1,))\n    result1 = progress1.resume(return_value=10)\n    assert isinstance(result1, pydantic_monty.MontyComplete)\n    assert result1.output == snapshot(10)\n\n    # Second run with different input\n    progress2 = m.start(inputs={'x': 2})\n    assert isinstance(progress2, pydantic_monty.FunctionSnapshot)\n    assert progress2.args == snapshot((2,))\n    result2 = progress2.resume(return_value=20)\n    assert isinstance(result2, pydantic_monty.MontyComplete)\n    assert result2.output == snapshot(20)\n\n\n@pytest.mark.parametrize(\n    'code,expected',\n    [\n        ('1', 1),\n        ('\"hello\"', 'hello'),\n        ('[1, 2, 3]', [1, 2, 3]),\n        ('{\"a\": 1}', {'a': 1}),\n        ('None', None),\n        ('True', True),\n    ],\n)\ndef test_start_returns_complete_for_various_types(code: str, expected: Any):\n    m = pydantic_monty.Monty(code)\n    result = m.start()\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == expected\n\n\ndef test_start_progress_resume_with_exception_caught():\n    \"\"\"Test that resuming with an exception is caught by try/except.\"\"\"\n    code = \"\"\"\ntry:\n    result = external_func()\nexcept ValueError:\n    caught = True\ncaught\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    # Resume with an exception using keyword argument\n    result = progress.resume(exception=ValueError('test error'))\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(True)\n\n\ndef test_start_progress_resume_exception_propagates_uncaught():\n    \"\"\"Test that uncaught exceptions from resume() propagate to caller.\"\"\"\n    code = 'external_func()'\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    # Resume with an exception that won't be caught - wrapped in MontyRuntimeError\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        progress.resume(exception=ValueError('uncaught error'))\n    inner = exc_info.value.exception()\n    assert isinstance(inner, ValueError)\n    assert inner.args[0] == snapshot('uncaught error')\n\n\ndef test_resume_none():\n    code = 'external_func()'\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    result = progress.resume(return_value=None)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(None)\n\n\ndef test_invalid_resume_args():\n    \"\"\"Test that resume() with no args returns None.\"\"\"\n    code = 'external_func()'\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    # no args provided\n    with pytest.raises(TypeError) as exc_info:\n        progress.resume()  # pyright: ignore[reportCallIssue]\n    assert exc_info.value.args[0] == snapshot('resume() accepts either return_value or exception, not both')\n\n    # Both arguments provided\n    with pytest.raises(TypeError) as exc_info:\n        progress.resume(return_value=42, exception=ValueError('error'))  # pyright: ignore[reportCallIssue]\n    assert exc_info.value.args[0] == snapshot('resume() accepts either return_value or exception, not both')\n\n    # invalid kwarg provided\n    with pytest.raises(TypeError) as exc_info:\n        progress.resume(invalid_kwarg=42)  # pyright: ignore[reportCallIssue]\n    assert exc_info.value.args[0] == snapshot('resume() accepts either return_value or exception, not both')\n\n\ndef test_start_progress_resume_exception_in_nested_try():\n    \"\"\"Test exception handling in nested try/except blocks.\"\"\"\n    code = \"\"\"\nouter_caught = False\nfinally_ran = False\ntry:\n    try:\n        external_func()\n    except TypeError:\n        pass  # Won't catch ValueError\n    finally:\n        finally_ran = True\nexcept ValueError:\n    outer_caught = True\n(outer_caught, finally_ran)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    result = progress.resume(exception=ValueError('propagates to outer'))\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot((True, True))\n\n\ndef test_name_lookup():\n    m = pydantic_monty.Monty('x = foo; x')\n    p = m.start()\n    assert isinstance(p, pydantic_monty.NameLookupSnapshot)\n    p2 = p.resume(value=42)\n    assert isinstance(p2, pydantic_monty.MontyComplete)\n    assert p2.output == 42\n\n\ndef test_ext_function_alt_name():\n    \"\"\"Test that a NameLookup can resolve to a function whose __name__ differs\n    from the variable it was assigned to.  The VM should yield a FunctionCall\n    with the *function's* name (not the variable name).\"\"\"\n    m = pydantic_monty.Monty('x = foobar; x()')\n    p = m.start()\n    assert isinstance(p, pydantic_monty.NameLookupSnapshot)\n\n    def not_foobar():\n        return 42\n\n    p2 = p.resume(value=not_foobar)\n    # The function is called via HeapData::ExtFunction, yielding a FunctionSnapshot\n    assert isinstance(p2, pydantic_monty.FunctionSnapshot)\n    assert p2.function_name == snapshot('not_foobar')\n    assert p2.args == snapshot(())\n    assert p2.kwargs == snapshot({})\n\n    result = p2.resume(return_value=42)\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == snapshot(42)\n"
  },
  {
    "path": "crates/monty-python/tests/test_threading.py",
    "content": "import os\nimport threading\nimport time\nfrom functools import partial\nfrom typing import cast\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n# I don't see a way to run these tests reliably on CI since github actions only has one CPU\n# perhaps we could use ubuntu-24.04-arm once the repo is open source (it's currently not supported for private repos)\n# https://docs.github.com/en/actions/reference/runners/github-hosted-runners\npytestmark = pytest.mark.skipif('CI' in os.environ, reason='on CI')\n\n\ndef test_parallel_exec():\n    \"\"\"Run code directly, run it in parallel, check that parallel execution not much slower.\"\"\"\n    code = \"\"\"\nx = 0\nfor i in range(200_000):\n    x += 1\nx\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    start = time.perf_counter()\n    result = m.run()\n    diff = time.perf_counter() - start\n    assert result == 200_000\n\n    threads = [threading.Thread(target=m.run) for _ in range(4)]\n    start = time.perf_counter()\n    for t in threads:\n        t.start()\n    for t in threads:\n        t.join()\n    diff_parallel = time.perf_counter() - start\n    # check that running the function in parallel 4 times is less than 1.5x slower than running it once\n    time_multiple = diff_parallel / diff\n    assert time_multiple < 1.5, 'Execution should not be slower in parallel'\n\n\ndef test_parallel_exec_print():\n    \"\"\"Run code directly, run it in parallel, check that parallel execution not much slower.\"\"\"\n    code = \"\"\"\nx = 0\nfor i in range(200_000):\n    x += 1\nprint(x)\n\"\"\"\n    captured: list[str] = []\n\n    def print_callback(file: str, content: str):\n        captured.append(f'{file}: {content}')\n\n    m = pydantic_monty.Monty(code)\n    start = time.perf_counter()\n    result = m.run(print_callback=print_callback)\n    diff = time.perf_counter() - start\n    assert result is None\n    assert captured == snapshot(['stdout: 200000', 'stdout: \\n'])\n\n    threads = [threading.Thread(target=partial(m.run, print_callback=print_callback)) for _ in range(4)]\n    start = time.perf_counter()\n    for t in threads:\n        t.start()\n    for t in threads:\n        t.join()\n    diff_parallel = time.perf_counter() - start\n    # check that running the function in parallel 4 times is less than 1.5x slower than running it once\n    time_multiple = diff_parallel / diff\n    assert time_multiple < 1.5, 'Execution should not be slower in parallel'\n\n\ndef double(a: int) -> int:\n    return a * 2\n\n\ndef test_parallel_exec_ext_functions():\n    \"\"\"Run code directly, run it in parallel, check that parallel execution not much slower.\"\"\"\n    code = \"\"\"\nx = 0\nfor i in range(100_000):\n    x += 1\nx = double(x)\nfor i in range(100_000):\n    x += 1\nx\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    start = time.perf_counter()\n    result = m.run(external_functions={'double': double})\n    diff = time.perf_counter() - start\n    assert result == 300_000\n\n    threads = [threading.Thread(target=partial(m.run, external_functions={'double': double})) for _ in range(4)]\n    start = time.perf_counter()\n    for t in threads:\n        t.start()\n    for t in threads:\n        t.join()\n    diff_parallel = time.perf_counter() - start\n    # check that running the function in parallel 4 times is less than 1.5x slower than running it once\n    time_multiple = diff_parallel / diff\n    assert time_multiple < 1.5, 'Execution should not be slower in parallel'\n\n\ndef test_parallel_exec_start():\n    \"\"\"Run code directly, run it in parallel, check that parallel execution not much slower.\"\"\"\n    code = \"\"\"\nx = 0\nfor i in range(200_000):\n    x += 1\ndouble(x)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    start = time.perf_counter()\n    progress = m.start()\n    diff = time.perf_counter() - start\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n\n    threads = [threading.Thread(target=m.start) for _ in range(4)]\n    start = time.perf_counter()\n    for t in threads:\n        t.start()\n    for t in threads:\n        t.join()\n    diff_parallel = time.perf_counter() - start\n    # check that running the function in parallel 4 times is less than 1.5x slower than running it once\n    time_multiple = diff_parallel / diff\n    assert time_multiple < 1.5, 'Execution should not be slower in parallel'\n\n\ndef test_parallel_exec_start_resume():\n    \"\"\"Run code directly, run it in parallel, check that parallel execution not much slower.\"\"\"\n    code = \"\"\"\nx = double(1)\nfor i in range(200_000):\n    x += 1\nx\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    progress = m.start()\n    assert isinstance(progress, pydantic_monty.FunctionSnapshot)\n    start = time.perf_counter()\n    result = progress.resume(return_value=2)\n    diff = time.perf_counter() - start\n    assert isinstance(result, pydantic_monty.MontyComplete)\n    assert result.output == 200_002\n\n    progresses = cast(list[pydantic_monty.FunctionSnapshot], [m.start() for _ in range(4)])\n\n    threads = [threading.Thread(target=partial(p.resume, return_value=2)) for p in progresses]\n    start = time.perf_counter()\n    for t in threads:\n        t.start()\n    for t in threads:\n        t.join()\n    diff_parallel = time.perf_counter() - start\n    # check that running the function in parallel 4 times is less than 1.5x slower than running it once\n    time_multiple = diff_parallel / diff\n    assert time_multiple < 1.5, 'Execution should not be slower in parallel'\n"
  },
  {
    "path": "crates/monty-python/tests/test_type_check.py",
    "content": "import pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_type_check_no_errors():\n    \"\"\"Type checking code with no errors returns None.\"\"\"\n    m = pydantic_monty.Monty('x = 1')\n    assert m.type_check() is None\n\n\ndef test_type_check_with_errors():\n    \"\"\"Type checking code with type errors raises MontyTypingError.\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[unsupported-operator]: Unsupported `+` operation\n --> main.py:1:1\n  |\n1 | \"hello\" + 1\n  | -------^^^-\n  | |         |\n  | |         Has type `Literal[1]`\n  | Has type `Literal[\"hello\"]`\n  |\ninfo: rule `unsupported-operator` is enabled by default\n\n\"\"\")\n\n\ndef test_type_check_function_return_type():\n    \"\"\"Type checking detects mismatched return types.\"\"\"\n    code = \"\"\"\ndef foo() -> int:\n    return \"not an int\"\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[invalid-return-type]: Return type does not match returned value\n --> main.py:2:14\n  |\n2 | def foo() -> int:\n  |              --- Expected `int` because of return type\n3 |     return \"not an int\"\n  |            ^^^^^^^^^^^^ expected `int`, found `Literal[\"not an int\"]`\n  |\ninfo: rule `invalid-return-type` is enabled by default\n\n\"\"\")\n\n\ndef test_type_check_undefined_variable():\n    \"\"\"Type checking detects undefined variables.\"\"\"\n    m = pydantic_monty.Monty('print(undefined_var)')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[unresolved-reference]: Name `undefined_var` used when not defined\n --> main.py:1:7\n  |\n1 | print(undefined_var)\n  |       ^^^^^^^^^^^^^\n  |\ninfo: rule `unresolved-reference` is enabled by default\n\n\"\"\")\n\n\ndef test_type_check_valid_function():\n    \"\"\"Type checking valid function returns None.\"\"\"\n    code = \"\"\"\ndef add(a: int, b: int) -> int:\n    return a + b\n\nadd(1, 2)\n\"\"\"\n    m = pydantic_monty.Monty(code)\n    assert m.type_check() is None\n\n\ndef test_type_check_with_prefix_code():\n    \"\"\"Type checking with prefix code for input declarations.\"\"\"\n    m = pydantic_monty.Monty('result = x + 1')\n    # Without prefix, x is undefined\n    with pytest.raises(pydantic_monty.MontyTypingError):\n        m.type_check()\n    # With prefix declaring x as a variable, it should pass\n    assert m.type_check(prefix_code='x = 0') is None\n\n\ndef test_type_check_display_invalid_format():\n    \"\"\"Invalid format string on display() raises ValueError.\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    with pytest.raises(ValueError) as val_exc:\n        exc_info.value.display('invalid_format')  # pyright: ignore[reportArgumentType]\n    assert str(val_exc.value) == snapshot('Unknown format: invalid_format')\n\n\ndef test_type_check_display_concise_format():\n    \"\"\"Type checking with concise format via display().\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    assert exc_info.value.display('concise') == snapshot(\n        'main.py:1:1: error[unsupported-operator] Operator `+` is not supported between objects of type `Literal[\"hello\"]` and `Literal[1]`\\n'\n    )\n\n\n# === MontyTypingError tests ===\n\n\ndef test_monty_typing_error_is_monty_error_subclass():\n    \"\"\"MontyTypingError is a subclass of MontyError.\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    error = exc_info.value\n    assert isinstance(error, pydantic_monty.MontyError)\n    assert isinstance(error, Exception)\n\n\ndef test_monty_typing_error_repr():\n    \"\"\"MontyTypingError has proper repr with truncation.\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    # repr truncates at 50 chars\n    assert repr(exc_info.value) == snapshot(\"\"\"\\\nMontyTypingError(error[unsupported-operator]: Unsupported `+` operation\n --> main.py:1:1\n  |\n1 | \"hello\" + 1\n  | -------^^^-\n  | |         |\n  | |         Has type `Literal[1]`\n  | Has type `Literal[\"hello\"]`\n  |\ninfo: rule `unsupported-operator` is enabled by default\n\n)\\\n\"\"\")\n\n\ndef test_monty_typing_error_caught_as_monty_error():\n    \"\"\"MontyTypingError can be caught as MontyError.\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyError):\n        m.type_check()\n\n\ndef test_monty_typing_error_display_default():\n    \"\"\"MontyTypingError display() defaults to full format.\"\"\"\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        m.type_check()\n    # Default display should match str()\n    assert exc_info.value.display() == str(exc_info.value)\n\n\n# === Constructor type_check parameter tests ===\n\n\ndef test_constructor_type_check_default_false():\n    \"\"\"Type checking is disabled by default in constructor.\"\"\"\n    # This should NOT raise during construction (type_check=False is default)\n    m = pydantic_monty.Monty('\"hello\" + 1')\n    # But we can still call type_check() manually later\n    with pytest.raises(pydantic_monty.MontyTypingError):\n        m.type_check()\n\n\ndef test_constructor_type_check_explicit_true():\n    \"\"\"Explicit type_check=True raises on type errors.\"\"\"\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        pydantic_monty.Monty('\"hello\" + 1', type_check=True)\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[unsupported-operator]: Unsupported `+` operation\n --> main.py:1:1\n  |\n1 | \"hello\" + 1\n  | -------^^^-\n  | |         |\n  | |         Has type `Literal[1]`\n  | Has type `Literal[\"hello\"]`\n  |\ninfo: rule `unsupported-operator` is enabled by default\n\n\"\"\")\n\n\ndef test_constructor_type_check_explicit_false():\n    \"\"\"Explicit type_check=False skips type checking during construction.\"\"\"\n    # This should NOT raise during construction\n    m = pydantic_monty.Monty('\"hello\" + 1', type_check=False)\n    # But we can still call type_check() manually later\n    with pytest.raises(pydantic_monty.MontyTypingError):\n        m.type_check()\n\n\ndef test_constructor_default_allows_run_with_inputs():\n    \"\"\"Default (type_check=False) allows running code that would fail type checking.\"\"\"\n    # Code with undefined variable - type checking would fail\n    m = pydantic_monty.Monty('x + 1', inputs=['x'])\n    # But runtime works fine with the input provided\n    result = m.run(inputs={'x': 5})\n    assert result == 6\n\n\ndef test_constructor_type_check_stubs():\n    \"\"\"type_check_stubs provides declarations for type checking.\"\"\"\n    # Without prefix, this would fail type checking (x is undefined)\n    # Use assignment to define x, not just type annotation\n    m = pydantic_monty.Monty('result = x + 1', type_check=True, type_check_stubs='x = 0')\n    # Should construct successfully because prefix declares x\n    assert m is not None\n\n\ndef test_constructor_type_check_stubs_with_external_function():\n    \"\"\"type_check_stubs can declare external function signatures.\"\"\"\n    # Define fetch as a function that takes a string and returns a string\n    prefix = \"\"\"\ndef fetch(url: str) -> str:\n    return ''\n\"\"\"\n    m = pydantic_monty.Monty(\n        'result = fetch(\"https://example.com\")',\n        type_check=True,\n        type_check_stubs=prefix,\n    )\n    assert m is not None\n\n\ndef test_constructor_type_check_stubs_invalid():\n    \"\"\"type_check_stubs with wrong types still catches errors.\"\"\"\n    # Prefix defines x as str, but code tries to use it with int addition\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        pydantic_monty.Monty(\n            'result: int = x + 1',\n            type_check=True,\n            type_check_stubs='x = \"hello\"',\n        )\n    # Should fail because str + int is invalid\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[unsupported-operator]: Unsupported `+` operation\n --> main.py:1:15\n  |\n1 | result: int = x + 1\n  |               -^^^-\n  |               |   |\n  |               |   Has type `Literal[1]`\n  |               Has type `Literal[\"hello\"]`\n  |\ninfo: rule `unsupported-operator` is enabled by default\n\n\"\"\")\n\n\ndef test_inject_stubs_offset():\n    type_definitions = \"\"\"\\\nfrom typing import Any\n\nMessages = list[dict[str, Any]]\n\nasync def call_llm(prompt: str, messages: Messages) -> str | Messages:\n    ...\n\nprompt: str = ''\n\"\"\"\n\n    code = \"\"\"\\\nasync def agent(prompt: str, messages: Messages):\n    while True:\n        print(f'messages so far: {messages}')\n        output = await call_llm(prompt, messages)\n        if isinstance(output, str):\n            return output\n        messages.extend(output)\n\nawait agent(prompt, [])\n\"\"\"\n    pydantic_monty.Monty(\n        code,\n        inputs=['prompt'],\n        script_name='agent.py',\n        type_check=True,\n        type_check_stubs=type_definitions,\n    )\n\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        pydantic_monty.Monty(\n            code.replace('Messages', 'MXessages'),\n            inputs=['prompt'],\n            script_name='agent.py',\n            type_check=True,\n            type_check_stubs=type_definitions,\n        )\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[unresolved-reference]: Name `MXessages` used when not defined\n --> agent.py:1:40\n  |\n1 | async def agent(prompt: str, messages: MXessages):\n  |                                        ^^^^^^^^^\n2 |     while True:\n3 |         print(f'messages so far: {messages}')\n  |\ninfo: rule `unresolved-reference` is enabled by default\n\n\"\"\")\n\n    code_call_func_wrong = 'await call_llm(prompt, 42)'\n\n    with pytest.raises(pydantic_monty.MontyTypingError) as exc_info:\n        pydantic_monty.Monty(\n            code_call_func_wrong,\n            inputs=['prompt'],\n            script_name='agent.py',\n            type_check=True,\n            type_check_stubs=type_definitions,\n        )\n    assert str(exc_info.value) == snapshot(\"\"\"\\\nerror[invalid-argument-type]: Argument to function `call_llm` is incorrect\n --> agent.py:1:24\n  |\n1 | await call_llm(prompt, 42)\n  |                        ^^ Expected `list[dict[str, Any]]`, found `Literal[42]`\n  |\ninfo: Function defined here\n --> type_stubs.pyi:5:11\n  |\n3 | Messages = list[dict[str, Any]]\n4 |\n5 | async def call_llm(prompt: str, messages: Messages) -> str | Messages:\n  |           ^^^^^^^^              ------------------ Parameter declared here\n6 |     ...\n  |\ninfo: rule `invalid-argument-type` is enabled by default\n\n\"\"\")\n"
  },
  {
    "path": "crates/monty-python/tests/test_types.py",
    "content": "import pytest\nfrom inline_snapshot import snapshot\n\nimport pydantic_monty\n\n\ndef test_none_input():\n    m = pydantic_monty.Monty('x is None', inputs=['x'])\n    assert m.run(inputs={'x': None}) is True\n\n\ndef test_none_output():\n    m = pydantic_monty.Monty('None')\n    assert m.run() is None\n\n\ndef test_bool_true():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': True})\n    assert result is True\n    assert type(result) is bool\n\n\ndef test_bool_false():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': False})\n    assert result is False\n    assert type(result) is bool\n\n\ndef test_int():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': 42}) == snapshot(42)\n    assert m.run(inputs={'x': -100}) == snapshot(-100)\n    assert m.run(inputs={'x': 0}) == snapshot(0)\n\n\ndef test_float():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': 3.14}) == snapshot(3.14)\n    assert m.run(inputs={'x': -2.5}) == snapshot(-2.5)\n    assert m.run(inputs={'x': 0.0}) == snapshot(0.0)\n\n\ndef test_string():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': 'hello'}) == snapshot('hello')\n    assert m.run(inputs={'x': ''}) == snapshot('')\n    assert m.run(inputs={'x': 'unicode: éè'}) == snapshot('unicode: éè')\n\n\ndef test_bytes():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': b'hello'}) == snapshot(b'hello')\n    assert m.run(inputs={'x': b''}) == snapshot(b'')\n    assert m.run(inputs={'x': b'\\x00\\x01\\x02'}) == snapshot(b'\\x00\\x01\\x02')\n\n\ndef test_list():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': [1, 2, 3]}) == snapshot([1, 2, 3])\n    assert m.run(inputs={'x': []}) == snapshot([])\n    assert m.run(inputs={'x': ['a', 'b']}) == snapshot(['a', 'b'])\n\n\ndef test_tuple():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': (1, 2, 3)}) == snapshot((1, 2, 3))\n    assert m.run(inputs={'x': ()}) == snapshot(())\n    assert m.run(inputs={'x': ('a',)}) == snapshot(('a',))\n\n\ndef test_dict():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': {'a': 1, 'b': 2}}) == snapshot({'a': 1, 'b': 2})\n    assert m.run(inputs={'x': {}}) == snapshot({})\n\n\ndef test_set():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': {1, 2, 3}}) == snapshot({1, 2, 3})\n    assert m.run(inputs={'x': set()}) == snapshot(set())\n\n\ndef test_frozenset():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    assert m.run(inputs={'x': frozenset([1, 2, 3])}) == snapshot(frozenset({1, 2, 3}))\n    assert m.run(inputs={'x': frozenset()}) == snapshot(frozenset())\n\n\ndef test_ellipsis_input():\n    m = pydantic_monty.Monty('x is ...', inputs=['x'])\n    assert m.run(inputs={'x': ...}) is True\n\n\ndef test_ellipsis_output():\n    m = pydantic_monty.Monty('...')\n    assert m.run() is ...\n\n\ndef test_nested_list():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    nested = [[1, 2], [3, [4, 5]]]\n    assert m.run(inputs={'x': nested}) == snapshot([[1, 2], [3, [4, 5]]])\n\n\ndef test_nested_dict():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    nested = {'a': {'b': {'c': 1}}}\n    assert m.run(inputs={'x': nested}) == snapshot({'a': {'b': {'c': 1}}})\n\n\ndef test_mixed_nested():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    mixed = {'list': [1, 2], 'tuple': (3, 4), 'nested': {'set': {5, 6}}}\n    result = m.run(inputs={'x': mixed})\n    assert result['list'] == snapshot([1, 2])\n    assert result['tuple'] == snapshot((3, 4))\n    assert result['nested']['set'] == snapshot({5, 6})\n\n\ndef test_list_output():\n    m = pydantic_monty.Monty('[1, 2, 3]')\n    assert m.run() == snapshot([1, 2, 3])\n\n\ndef test_dict_output():\n    m = pydantic_monty.Monty(\"{'a': 1, 'b': 2}\")\n    assert m.run() == snapshot({'a': 1, 'b': 2})\n\n\ndef test_tuple_output():\n    m = pydantic_monty.Monty('(1, 2, 3)')\n    assert m.run() == snapshot((1, 2, 3))\n\n\ndef test_set_output():\n    m = pydantic_monty.Monty('{1, 2, 3}')\n    assert m.run() == snapshot({1, 2, 3})\n\n\n# === Exception types ===\n\n\ndef test_exception_input():\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    exc = ValueError('test error')\n    result = m.run(inputs={'x': exc})\n    assert isinstance(result, ValueError)\n    assert str(result) == snapshot('test error')\n\n\ndef test_exception_output():\n    m = pydantic_monty.Monty('ValueError(\"created\")')\n    result = m.run()\n    assert isinstance(result, ValueError)\n    assert str(result) == snapshot('created')\n\n\n@pytest.mark.parametrize('exc_class', [ValueError, TypeError, RuntimeError, AttributeError], ids=repr)\ndef test_exception_roundtrip(exc_class: type[Exception]):\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    exc = exc_class('message')\n    result = m.run(inputs={'x': exc})\n    assert type(result) is exc_class\n    assert str(result) == snapshot('message')\n\n\ndef test_exception_subclass_input():\n    \"\"\"Custom exception subtypes are converted to their nearest supported base.\"\"\"\n\n    class MyError(ValueError):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    exc = MyError('custom')\n    result = m.run(inputs={'x': exc})\n    # Custom exception becomes ValueError (nearest supported type)\n    assert type(result) is ValueError\n    assert str(result) == snapshot('custom')\n\n\n# === Subtype coercion ===\n# Monty converts Python subclasses to their base types since it doesn't\n# have Python's class system.\n\n\ndef test_int_subclass_input():\n    class MyInt(int):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': MyInt(42)})\n    assert type(result) is int\n    assert result == snapshot(42)\n\n\ndef test_str_subclass_input():\n    class MyStr(str):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': MyStr('hello')})\n    assert type(result) is str\n    assert result == snapshot('hello')\n\n\ndef test_list_subclass_input():\n    class MyList(list[int]):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': MyList([1, 2, 3])})\n    assert type(result) is list\n    assert result == snapshot([1, 2, 3])\n\n\ndef test_dict_subclass_input():\n    class MyDict(dict[str, int]):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': MyDict({'a': 1})})\n    assert type(result) is dict\n    assert result == snapshot({'a': 1})\n\n\ndef test_tuple_subclass_input():\n    class MyTuple(tuple[int, ...]):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': MyTuple((1, 2))})\n    assert type(result) is tuple\n    assert result == snapshot((1, 2))\n\n\ndef test_set_subclass_input():\n    class MySet(set[int]):\n        pass\n\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': MySet({1, 2})})\n    assert type(result) is set\n    assert result == snapshot({1, 2})\n\n\ndef test_bool_preserves_type():\n    \"\"\"Bool is a subclass of int but should be preserved as bool.\"\"\"\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': True})\n    assert type(result) is bool\n    assert result is True\n\n\ndef test_return_int():\n    m = pydantic_monty.Monty('x = 4\\ntype(x)')\n    result = m.run()\n    assert result is int\n\n    m = pydantic_monty.Monty('int')\n    result = m.run()\n    assert result is int\n\n\ndef test_return_exception():\n    m = pydantic_monty.Monty('x = ValueError()\\ntype(x)')\n    result = m.run()\n    assert result is ValueError\n\n    m = pydantic_monty.Monty('ValueError')\n    result = m.run()\n    assert result is ValueError\n\n\ndef test_return_builtin():\n    m = pydantic_monty.Monty('len')\n    result = m.run()\n    assert result is len\n\n\n# === BigInt (arbitrary precision integers) ===\n\n\ndef test_bigint_input():\n    \"\"\"Passing a large integer (> i64::MAX) as input.\"\"\"\n    big = 2**100\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': big})\n    assert result == big\n    assert type(result) is int\n\n\ndef test_bigint_output():\n    \"\"\"Returning a large integer computed inside Monty.\"\"\"\n    m = pydantic_monty.Monty('2**100')\n    result = m.run()\n    assert result == 2**100\n    assert type(result) is int\n\n\ndef test_bigint_negative_input():\n    \"\"\"Passing a large negative integer as input.\"\"\"\n    big_neg = -(2**100)\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': big_neg})\n    assert result == big_neg\n    assert type(result) is int\n\n\ndef test_int_overflow_to_bigint():\n    \"\"\"Small int input that overflows to bigint during computation.\"\"\"\n    max_i64 = 9223372036854775807\n    m = pydantic_monty.Monty('x + 1', inputs=['x'])\n    result = m.run(inputs={'x': max_i64})\n    assert result == max_i64 + 1\n    assert type(result) is int\n\n\ndef test_bigint_arithmetic():\n    \"\"\"BigInt arithmetic operations.\"\"\"\n    big = 2**100\n    m = pydantic_monty.Monty('x * 2 + y', inputs=['x', 'y'])\n    result = m.run(inputs={'x': big, 'y': big})\n    assert result == big * 2 + big\n    assert type(result) is int\n\n\ndef test_bigint_comparison():\n    \"\"\"Comparing bigints with regular ints.\"\"\"\n    big = 2**100\n    m = pydantic_monty.Monty('x > y', inputs=['x', 'y'])\n    assert m.run(inputs={'x': big, 'y': 42}) is True\n    assert m.run(inputs={'x': 42, 'y': big}) is False\n\n\ndef test_bigint_in_collection():\n    \"\"\"BigInts inside collections.\"\"\"\n    big = 2**100\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': [big, 42, big * 2]})\n    assert result == [big, 42, big * 2]\n    assert type(result[0]) is int\n\n\ndef test_bigint_as_dict_key():\n    \"\"\"BigInt as dictionary key.\"\"\"\n    big = 2**100\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result = m.run(inputs={'x': {big: 'value'}})\n    assert result == {big: 'value'}\n    assert big in result\n\n\ndef test_bigint_hash_consistency_small_values():\n    \"\"\"Hash of small values computed as BigInt must match regular int hash.\n\n    This is critical for dict key lookups: inserting with int and looking up\n    with a computed BigInt (or vice versa) must work correctly.\n    \"\"\"\n    # Value 42 computed via BigInt arithmetic\n    big = 2**100\n    m = pydantic_monty.Monty('(x - x) + 42', inputs=['x'])\n    computed_42 = m.run(inputs={'x': big})\n\n    # Hash must match\n    assert hash(computed_42) == hash(42), 'hash of computed int must match literal'\n\n    # Dict lookup must work both ways\n    d = {42: 'value'}\n    assert d[computed_42] == 'value', 'lookup with computed bigint finds int key'\n\n    d2 = {computed_42: 'value'}\n    assert d2[42] == 'value', 'lookup with int finds computed bigint key'\n\n\ndef test_bigint_hash_consistency_boundary():\n    \"\"\"Hash consistency at i64 boundary values.\"\"\"\n    max_i64 = 9223372036854775807\n\n    # Compute MAX_I64 via BigInt arithmetic\n    m = pydantic_monty.Monty('(x - 1)', inputs=['x'])\n    computed_max = m.run(inputs={'x': max_i64 + 1})\n\n    assert hash(computed_max) == hash(max_i64), 'hash at MAX_I64 boundary must match'\n\n\ndef test_bigint_hash_consistency_large_values():\n    \"\"\"Equal large BigInts must hash the same.\"\"\"\n    big1 = 2**100\n    big2 = 2**100\n\n    # Verify they hash the same in Python first\n    assert hash(big1) == hash(big2), 'precondition: equal bigints hash same in Python'\n\n    # Verify hashes match after round-trip through Monty\n    m = pydantic_monty.Monty('x', inputs=['x'])\n    result1 = m.run(inputs={'x': big1})\n    result2 = m.run(inputs={'x': big2})\n\n    assert hash(result1) == hash(result2), 'equal bigints from Monty must hash same'\n\n    # Dict lookup must work\n    d = {result1: 'value'}\n    assert d[result2] == 'value', 'lookup with equal bigint works'\n\n\n# === NamedTuple output ===\n\n\ndef test_namedtuple_sys_version_info():\n    \"\"\"sys.version_info returns a proper namedtuple with attribute access.\"\"\"\n    m = pydantic_monty.Monty('import sys; sys.version_info')\n    result = m.run()\n\n    # Should have named attribute access\n    assert hasattr(result, 'major')\n    assert hasattr(result, 'minor')\n    assert hasattr(result, 'micro')\n    assert hasattr(result, 'releaselevel')\n    assert hasattr(result, 'serial')\n\n    # Values should match Monty's Python version (3.14)\n    assert result.major == snapshot(3)\n    assert result.minor == snapshot(14)\n    assert result.micro == snapshot(0)\n    assert result.releaselevel == snapshot('final')\n    assert result.serial == snapshot(0)\n\n\ndef test_namedtuple_sys_version_info_index_access():\n    \"\"\"sys.version_info supports both index and attribute access.\"\"\"\n    m = pydantic_monty.Monty('import sys; sys.version_info')\n    result = m.run()\n\n    # Index access should work\n    assert result[0] == result.major\n    assert result[1] == result.minor\n    assert result[2] == result.micro\n\n\ndef test_namedtuple_sys_version_info_tuple_comparison():\n    \"\"\"sys.version_info can be compared to tuples.\"\"\"\n    m = pydantic_monty.Monty('import sys; (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)')\n    result = m.run()\n    assert result == snapshot((3, 14, 0))\n\n\n# === User-defined NamedTuple input ===\n\n\ndef test_namedtuple_custom_input_attribute_access():\n    \"\"\"User-defined NamedTuple with custom field names can be accessed by attribute.\"\"\"\n    from typing import NamedTuple\n\n    class Person(NamedTuple):\n        name: str\n        age: int\n\n    m = pydantic_monty.Monty('p.name', inputs=['p'])\n    assert m.run(inputs={'p': Person(name='Alice', age=30)}) == snapshot('Alice')\n\n    m = pydantic_monty.Monty('p.age', inputs=['p'])\n    assert m.run(inputs={'p': Person(name='Alice', age=30)}) == snapshot(30)\n\n\ndef test_namedtuple_custom_input_index_access():\n    \"\"\"User-defined NamedTuple supports both attribute and index access.\"\"\"\n    from typing import NamedTuple\n\n    class Point(NamedTuple):\n        x: int\n        y: int\n\n    m = pydantic_monty.Monty('p[0] + p[1]', inputs=['p'])\n    assert m.run(inputs={'p': Point(x=10, y=20)}) == snapshot(30)\n\n\ndef test_namedtuple_custom_input_multiple_fields():\n    \"\"\"NamedTuple with multiple custom field names works correctly.\"\"\"\n    from typing import NamedTuple\n\n    class Config(NamedTuple):\n        host: str\n        port: int\n        debug: bool\n        timeout: float\n\n    m = pydantic_monty.Monty(\"f'{c.host}:{c.port}'\", inputs=['c'])\n    result = m.run(inputs={'c': Config(host='localhost', port=8080, debug=True, timeout=30.0)})\n    assert result == snapshot('localhost:8080')\n\n    m = pydantic_monty.Monty('c.debug', inputs=['c'])\n    result = m.run(inputs={'c': Config(host='localhost', port=8080, debug=True, timeout=30.0)})\n    assert result is True\n\n\ndef test_namedtuple_custom_input_repr():\n    \"\"\"User-defined NamedTuple has correct repr with fully-qualified type name.\"\"\"\n    from typing import NamedTuple\n\n    class Item(NamedTuple):\n        name: str\n        price: float\n\n    m = pydantic_monty.Monty('repr(item)', inputs=['item'])\n    result = m.run(inputs={'item': Item(name='widget', price=9.99)})\n    # Monty uses the full qualified name (module.ClassName) for the type\n    assert result == snapshot(\"test_types.Item(name='widget', price=9.99)\")\n\n\ndef test_namedtuple_custom_input_len():\n    \"\"\"User-defined NamedTuple supports len().\"\"\"\n    from typing import NamedTuple\n\n    class Triple(NamedTuple):\n        a: int\n        b: int\n        c: int\n\n    m = pydantic_monty.Monty('len(t)', inputs=['t'])\n    assert m.run(inputs={'t': Triple(a=1, b=2, c=3)}) == snapshot(3)\n\n\ndef test_namedtuple_custom_input_roundtrip():\n    \"\"\"User-defined NamedTuple can be passed through and returned.\"\"\"\n    from typing import NamedTuple\n\n    class Pair(NamedTuple):\n        first: int\n        second: int\n\n    m = pydantic_monty.Monty('p', inputs=['p'])\n    result = m.run(inputs={'p': Pair(first=1, second=2)})\n    # Returns a namedtuple-like object (not the same Python class)\n    assert result[0] == snapshot(1)\n    assert result[1] == snapshot(2)\n    assert result.first == snapshot(1)\n    assert result.second == snapshot(2)\n\n\ndef test_namedtuple_custom_missing_attr_error():\n    \"\"\"Accessing non-existent attribute on custom NamedTuple raises AttributeError.\"\"\"\n    from typing import NamedTuple\n\n    class Simple(NamedTuple):\n        value: int\n\n    m = pydantic_monty.Monty('s.nonexistent', inputs=['s'])\n    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:\n        m.run(inputs={'s': Simple(value=42)})\n    # Monty uses the full qualified name (module.ClassName) for the type\n    assert \"AttributeError: 'test_types.Simple' object has no attribute 'nonexistent'\" in str(exc_info.value)\n"
  },
  {
    "path": "crates/monty-type-checking/Cargo.toml",
    "content": "[package]\nname = \"monty_type_checking\"\nreadme = \"../../README.md\"\nversion = { workspace = true }\nrust-version = { workspace = true }\nedition = { workspace = true }\nauthors = { workspace = true }\nlicense = { workspace = true }\ndescription = { workspace = true }\nkeywords = { workspace = true }\ncategories = { workspace = true }\nhomepage = { workspace = true }\nrepository = { workspace = true }\n\n[lib]\nname = \"monty_type_checking\"\npath = \"src/lib.rs\"\n\n[dependencies]\nmonty_typeshed = { path = \"../monty-typeshed\" }\nruff_python_ast = { workspace = true }\nruff_db = { workspace = true }\nruff_text_size = { workspace = true }\nty_python_semantic = { workspace = true }\nty_module_resolver = { workspace = true }\nsalsa = { workspace = true }\n\n[dev-dependencies]\npretty_assertions = { workspace = true }\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/monty-type-checking/src/db.rs",
    "content": "use std::{fmt, sync::Arc};\n\nuse ruff_db::{\n    Db as SourceDb,\n    files::{File, Files},\n    system::{DbWithTestSystem, System, TestSystem},\n    vendored::VendoredFileSystem,\n};\nuse ruff_python_ast::PythonVersion;\nuse ty_module_resolver::{Db as ModuleResolverDb, SearchPaths};\nuse ty_python_semantic::{\n    AnalysisSettings, Db, Program, default_lint_registry,\n    lint::{LintRegistry, RuleSelection},\n};\n\n/// Very simple in-memory salsa/ty database.\n///\n/// Mostly taken from\n/// https://github.com/astral-sh/ruff/blob/7bacca9b625c2a658470afd99a0bf0aa0b4f1dbb/crates/ty_python_semantic/src/db.rs#L51\n#[salsa::db]\n#[derive(Clone)]\npub(crate) struct MemoryDb {\n    storage: salsa::Storage<Self>,\n    files: Files,\n    system: TestSystem,\n    vendored: VendoredFileSystem,\n    rule_selection: Arc<RuleSelection>,\n    analysis_settings: Arc<AnalysisSettings>,\n}\n\nimpl fmt::Debug for MemoryDb {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"TypeCheckingFailure\")\n            .field(\"files\", &self.files)\n            .field(\"system\", &self.system)\n            .field(\"vendored\", &self.vendored)\n            .field(\"rule_selection\", &self.rule_selection)\n            .field(\"analysis_settings\", &self.analysis_settings)\n            .finish_non_exhaustive()\n    }\n}\n\nimpl MemoryDb {\n    pub fn new() -> Self {\n        Self {\n            storage: salsa::Storage::new(None),\n            system: TestSystem::default(),\n            vendored: monty_typeshed::file_system().clone(),\n            files: Files::default(),\n            rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),\n            analysis_settings: AnalysisSettings::default().into(),\n        }\n    }\n}\n\nimpl DbWithTestSystem for MemoryDb {\n    fn test_system(&self) -> &TestSystem {\n        &self.system\n    }\n\n    fn test_system_mut(&mut self) -> &mut TestSystem {\n        &mut self.system\n    }\n}\n\n#[salsa::db]\nimpl SourceDb for MemoryDb {\n    fn vendored(&self) -> &VendoredFileSystem {\n        &self.vendored\n    }\n\n    fn system(&self) -> &dyn System {\n        &self.system\n    }\n\n    fn files(&self) -> &Files {\n        &self.files\n    }\n\n    fn python_version(&self) -> PythonVersion {\n        PythonVersion::PY314\n    }\n}\n\n#[salsa::db]\nimpl Db for MemoryDb {\n    fn should_check_file(&self, file: File) -> bool {\n        !file.path(self).is_vendored_path()\n    }\n\n    fn rule_selection(&self, _file: File) -> &RuleSelection {\n        &self.rule_selection\n    }\n\n    fn lint_registry(&self) -> &LintRegistry {\n        default_lint_registry()\n    }\n\n    fn analysis_settings(&self, _file: File) -> &AnalysisSettings {\n        &self.analysis_settings\n    }\n\n    fn verbose(&self) -> bool {\n        false\n    }\n}\n\n#[salsa::db]\nimpl ModuleResolverDb for MemoryDb {\n    fn search_paths(&self) -> &SearchPaths {\n        Program::get(self).search_paths(self)\n    }\n}\n\n#[salsa::db]\nimpl salsa::Database for MemoryDb {}\n"
  },
  {
    "path": "crates/monty-type-checking/src/lib.rs",
    "content": "mod db;\nmod type_check;\n\npub use crate::type_check::{SourceFile, TypeCheckingDiagnostics, type_check};\n"
  },
  {
    "path": "crates/monty-type-checking/src/type_check.rs",
    "content": "use std::{\n    fmt::{self, Display},\n    sync::{Arc, Mutex},\n};\n\nuse ruff_db::{\n    Db as SourceDb,\n    diagnostic::{\n        Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics,\n        UnifiedFile,\n    },\n    files::{File, FileRootKind, system_path_to_file},\n    system::{DbWithWritableSystem as _, SystemPathBuf},\n};\nuse ruff_text_size::{TextRange, TextSize};\nuse ty_module_resolver::SearchPathSettings;\nuse ty_python_semantic::{\n    Program, ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource, types::check_types,\n};\n\nuse crate::db::MemoryDb;\n\n/// Definition of a source file.\npub struct SourceFile<'a> {\n    /// source code\n    pub source_code: &'a str,\n    /// file path\n    pub path: &'a str,\n}\n\nimpl<'a> SourceFile<'a> {\n    /// Create a new source file.\n    #[must_use]\n    pub fn new(source_code: &'a str, path: &'a str) -> Self {\n        Self { source_code, path }\n    }\n}\n\n/// Type check some python source code, checking if it's valid to run with monty.\n///\n/// # Arguments\n/// * `python_source` - The python source code to type check.\n/// * `stubs_file` - Optional stubs file to use for type checking.\n///\n/// # Returns\n/// * `Ok(Some(TypeCheckingFailure))` - If there are typing errors.\n/// * `Ok(None)` - If there are no typing errors.\n/// * `Err(String)` - If there was an unexpected/internal error during type checking.\npub fn type_check(\n    python_source: &SourceFile<'_>,\n    stubs_file: Option<&SourceFile<'_>>,\n) -> Result<Option<TypeCheckingDiagnostics>, String> {\n    let mut db = MemoryDb::new();\n\n    // Files must be written under a directory that's registered as a search path for module\n    // resolution to work. We use \"/\" as the root directory so paths appear without a prefix.\n    let src_root = SystemPathBuf::from(\"/\");\n\n    // Register the source root for Salsa tracking - required for module resolution\n    db.files().try_add_root(&db, &src_root, FileRootKind::Project);\n\n    let search_paths = SearchPathSettings::new(vec![src_root.clone()])\n        .to_search_paths(db.system(), db.vendored())\n        .map_err(to_string)?;\n\n    // The API is confusing here - we have to load the \"program\" here like this, otherwise we get unwrap\n    // panics when calling `check_types`\n    Program::from_settings(\n        &db,\n        ProgramSettings {\n            python_version: PythonVersionWithSource {\n                version: db.python_version(),\n                source: PythonVersionSource::default(),\n            },\n            python_platform: PythonPlatform::default(),\n            search_paths,\n        },\n    );\n\n    // Build absolute paths for files under /\n    let main_path = src_root.join(python_source.path);\n    let main_source = python_source.source_code;\n\n    let code_offset: u32 = if let Some(stubs_file) = stubs_file {\n        let stubs_path = src_root.join(stubs_file.path);\n\n        // write the stub file\n        db.write_file(&stubs_path, stubs_file.source_code).map_err(to_string)?;\n\n        // prepend the stub import to the main source code\n        let stub_stem = stubs_file\n            .path\n            .split_once('.')\n            .map_or(stubs_file.path, |(before, _)| before);\n        let mut new_source = format!(\"from {stub_stem} import *\\n\");\n        let offset = u32::try_from(new_source.len()).map_err(to_string)?;\n        new_source.push_str(main_source);\n\n        // write the main source code\n        db.write_file(&main_path, &new_source).map_err(to_string)?;\n        // one line offset for errors vs. the original source code since we injected the stub import\n        offset\n    } else {\n        // write just the main source code\n        db.write_file(&main_path, main_source).map_err(to_string)?;\n        0\n    };\n\n    let main_file = system_path_to_file(&db, &main_path).map_err(to_string)?;\n    let mut diagnostics = check_types(&db, main_file);\n    diagnostics.retain(filter_diagnostics);\n\n    if diagnostics.is_empty() {\n        Ok(None)\n    } else {\n        // without all this errors would appear on the wrong line because we injected `from type_stubs import *`\n\n        // if we injected the stubs import, we need to write the actual source back to the file in the database\n        db.write_file(&main_path, main_source).map_err(to_string)?;\n        // and then adjust each span in the error message to account for the injected stubs import\n        if code_offset > 0 {\n            let offset = TextSize::new(code_offset);\n            for diagnostic in &mut diagnostics {\n                // Adjust spans in main diagnostic annotations (only for spans in the main file)\n                for ann in diagnostic.annotations_mut() {\n                    adjust_annotation_span(ann, main_file, offset);\n                }\n                // Adjust spans in sub-diagnostic annotations (e.g., \"info: Function defined here\")\n                for sub in diagnostic.sub_diagnostics_mut() {\n                    for ann in sub.annotations_mut() {\n                        adjust_annotation_span(ann, main_file, offset);\n                    }\n                }\n            }\n        }\n        // Sort diagnostics by line number\n        diagnostics.sort_by(|a, b| a.rendering_sort_key(&db).cmp(&b.rendering_sort_key(&db)));\n\n        Ok(Some(TypeCheckingDiagnostics::new(diagnostics, db)))\n    }\n}\n\nfn to_string(err: impl Display) -> String {\n    err.to_string()\n}\n\n/// Adjust the span of an annotation by subtracting the given offset.\n///\n/// This is used when we inject a stub import at the beginning of the source code,\n/// and need to adjust all spans to account for the injected code.\n/// Only adjusts spans that belong to the main file being type-checked.\nfn adjust_annotation_span(ann: &mut Annotation, main_file: File, offset: TextSize) {\n    let span = ann.get_span();\n    // Only adjust spans for the main file (not stubs or other files)\n    if let UnifiedFile::Ty(span_file) = span.file()\n        && *span_file == main_file\n        && let Some(range) = span.range()\n    {\n        let new_range = TextRange::new(range.start() - offset, range.end() - offset);\n        let new_span = span.clone().with_range(new_range);\n        ann.set_span(new_span);\n    }\n}\n\n/// Represents diagnostic details when type checking fails.\n#[derive(Clone)]\npub struct TypeCheckingDiagnostics {\n    /// The actual diagnostic message\n    diagnostics: Vec<Diagnostic>,\n    /// db used to display diagnostics, wrapped in Mutex for Sync so MontyTypingError is sendable\n    db: Arc<Mutex<MemoryDb>>,\n    /// How to format the output\n    format: DiagnosticFormat,\n    /// Whether to highlight the output with ansi colors\n    color: bool,\n}\n\n/// Debug output for TypeCheckingDiagnostics shows the pretty typing output, and no other values since\n/// this will be displayed when users are printing `Result<..., TypeCheckingDiagnostics>` etc. and the\n/// raw errors are not useful to end users.\nimpl fmt::Debug for TypeCheckingDiagnostics {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let config = self.config();\n        let db = self.db.lock().unwrap();\n        write!(\n            f,\n            \"TypeCheckingDiagnostics:\\n{}\",\n            DisplayDiagnostics::new(&*db, &config, &self.diagnostics)\n        )\n    }\n}\n\n/// To display true debugs details about the TypeCheckingDiagnostics\n#[derive(Debug)]\n#[expect(dead_code)]\npub struct DebugTypeCheckingDiagnostics<'a> {\n    diagnostics: &'a [Diagnostic],\n    db: Arc<Mutex<MemoryDb>>,\n    format: DiagnosticFormat,\n    color: bool,\n}\n\nimpl fmt::Display for TypeCheckingDiagnostics {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let db = self.db.lock().unwrap();\n        DisplayDiagnostics::new(&*db, &self.config(), &self.diagnostics).fmt(f)\n    }\n}\n\nimpl TypeCheckingDiagnostics {\n    fn new(diagnostics: Vec<Diagnostic>, db: MemoryDb) -> Self {\n        Self {\n            diagnostics,\n            db: Arc::new(Mutex::new(db)),\n            format: DiagnosticFormat::Full,\n            color: false,\n        }\n    }\n\n    fn config(&self) -> DisplayDiagnosticConfig {\n        DisplayDiagnosticConfig::new(\"monty\")\n            .format(self.format)\n            .color(self.color)\n    }\n\n    /// To display debug details for the TypeCheckingDiagnostics since debug is the pretty output\n    #[must_use]\n    pub fn debug_details(&self) -> DebugTypeCheckingDiagnostics<'_> {\n        DebugTypeCheckingDiagnostics {\n            diagnostics: &self.diagnostics,\n            db: self.db.clone(),\n            format: self.format,\n            color: self.color,\n        }\n    }\n\n    /// Set the format of the diagnostics.\n    #[must_use]\n    pub fn format(self, format: DiagnosticFormat) -> Self {\n        Self { format, ..self }\n    }\n\n    /// Set the format of the diagnostics from a string.\n    /// Valid formats: \"full\", \"concise\", \"azure\", \"json\", \"jsonlines\", \"rdjson\",\n    /// \"pylint\", \"gitlab\", \"github\".\n    pub fn format_from_str(self, format: &str) -> Result<Self, String> {\n        let format = match format.to_ascii_lowercase().as_str() {\n            \"full\" => DiagnosticFormat::Full,\n            \"concise\" => DiagnosticFormat::Concise,\n            \"azure\" => DiagnosticFormat::Azure,\n            \"json\" => DiagnosticFormat::Json,\n            \"jsonlines\" | \"json-lines\" => DiagnosticFormat::JsonLines,\n            \"rdjson\" => DiagnosticFormat::Rdjson,\n            \"pylint\" => DiagnosticFormat::Pylint,\n            // don't bother with the \"junit\" feature, please check the binary size and add it if you need this format\n            // \"junit\" => DiagnosticFormat::Junit,\n            \"gitlab\" => DiagnosticFormat::Gitlab,\n            \"github\" => DiagnosticFormat::Github,\n            _ => return Err(format!(\"Unknown format: {format}\")),\n        };\n        Ok(Self { format, ..self })\n    }\n\n    /// Set whether to highlight the output with ansi colors\n    #[must_use]\n    pub fn color(self, color: bool) -> Self {\n        Self { color, ..self }\n    }\n}\n\n/// Filter out diagnostics we want to ignore.\n///\n/// Should only be necessary until <https://github.com/astral-sh/ty/issues/2599> is fixed.\nfn filter_diagnostics(d: &Diagnostic) -> bool {\n    !(matches!(d.id(), DiagnosticId::InvalidSyntax)\n        && matches!(\n            d.primary_message(),\n            \"`await` statement outside of a function\" | \"`await` outside of an asynchronous function\"\n        ))\n}\n"
  },
  {
    "path": "crates/monty-type-checking/tests/bad_types.py",
    "content": "# This file contains intentional type errors to test the type checker.\n# Each section demonstrates a different category of type error.\n# ===\n\nimport sys\nfrom typing import assert_type\n\n\ndef takes_int(x: int) -> None:\n    pass\n\n\ndef takes_str(x: str) -> None:\n    pass\n\n\ndef takes_list_int(x: list[int]) -> None:\n    pass\n\n\n# Wrong primitive types\ntakes_int('hello')\ntakes_int(3.14)\ntakes_str(42)\ntakes_str([1, 2, 3])\n\n# Wrong container element types\ntakes_list_int(['a', 'b', 'c'])\ntakes_list_int([1.0, 2.0, 3.0])\n\n\n# === Invalid return types ===\n\n\ndef should_return_int() -> int:\n    return 'oops'\n\n\ndef should_return_str() -> str:\n    return 123\n\n\ndef should_return_list_int() -> list[int]:\n    return ['a', 'b']\n\n\ndef should_return_none() -> None:\n    return 42\n\n\n# === Type mismatches in expressions ===\n\n\ndef get_int() -> int:\n    return 42\n\n\ndef get_str() -> str:\n    return 'hello'\n\n\n# Binary operations with incompatible types\nresult1 = get_int() + get_str()\nresult2 = get_str() - get_int()\n\n\n# === assert_type failures ===\n\nx: int = 42\nassert_type(x, str)\n\ny: list[int] = [1, 2, 3]\nassert_type(y, list[str])\n\n\n# === Attribute errors ===\n\n\nclass MyClass:\n    def __init__(self) -> None:\n        self.value: int = 42\n\n\nobj = MyClass()\nz = obj.nonexistent_attr\n\n\n# === Too many / too few arguments ===\n\n\ndef takes_two(a: int, b: str) -> None:\n    pass\n\n\ntakes_two(1)\ntakes_two(1, 'hello', 'extra')\n\n\n# === Wrong keyword arguments ===\n\ntakes_two(a=1, c='wrong')\n\n\n# === Calling non-callable ===\n\nnot_callable: int = 42\nnot_callable()\n\nprint(sys.copyright)\n"
  },
  {
    "path": "crates/monty-type-checking/tests/bad_types_output.txt",
    "content": "bad_types.py:22:11: error[invalid-argument-type] Argument to function `takes_int` is incorrect: Expected `int`, found `Literal[\"hello\"]`\nbad_types.py:23:11: error[invalid-argument-type] Argument to function `takes_int` is incorrect: Expected `int`, found `float`\nbad_types.py:24:11: error[invalid-argument-type] Argument to function `takes_str` is incorrect: Expected `str`, found `Literal[42]`\nbad_types.py:25:11: error[invalid-argument-type] Argument to function `takes_str` is incorrect: Expected `str`, found `list[Unknown | int]`\nbad_types.py:28:16: error[invalid-argument-type] Argument to function `takes_list_int` is incorrect: Expected `list[int]`, found `list[int | str]`\nbad_types.py:29:16: error[invalid-argument-type] Argument to function `takes_list_int` is incorrect: Expected `list[int]`, found `list[int | float]`\nbad_types.py:36:12: error[invalid-return-type] Return type does not match returned value: expected `int`, found `Literal[\"oops\"]`\nbad_types.py:40:12: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Literal[123]`\nbad_types.py:44:12: error[invalid-return-type] Return type does not match returned value: expected `list[int]`, found `list[int | str]`\nbad_types.py:48:12: error[invalid-return-type] Return type does not match returned value: expected `None`, found `Literal[42]`\nbad_types.py:63:11: error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `str`\nbad_types.py:64:11: error[unsupported-operator] Operator `-` is not supported between objects of type `str` and `int`\nbad_types.py:70:1: error[type-assertion-failure] Type `str` does not match asserted type `Literal[42]`\nbad_types.py:73:1: error[type-assertion-failure] Type `list[str]` does not match asserted type `list[int]`\nbad_types.py:85:5: error[unresolved-attribute] Object of type `MyClass` has no attribute `nonexistent_attr`\nbad_types.py:95:1: error[missing-argument] No argument provided for required parameter `b` of function `takes_two`\nbad_types.py:96:23: error[too-many-positional-arguments] Too many positional arguments to function `takes_two`: expected 2, got 3\nbad_types.py:101:1: error[missing-argument] No argument provided for required parameter `b` of function `takes_two`\nbad_types.py:101:16: error[unknown-argument] Argument `c` does not match any known parameter of function `takes_two`\nbad_types.py:107:1: error[call-non-callable] Object of type `Literal[42]` is not callable\nbad_types.py:109:7: error[unresolved-attribute] Module `sys` has no member `copyright`\n"
  },
  {
    "path": "crates/monty-type-checking/tests/good_types.py",
    "content": "import asyncio\nimport os\nimport re\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any, assert_type\n\n# === Type checking helper functions ===\n\n\ndef check_int(x: int) -> None:\n    pass\n\n\ndef check_float(x: float) -> None:\n    pass\n\n\ndef check_str(x: str) -> None:\n    pass\n\n\ndef check_bool(x: bool) -> None:\n    pass\n\n\ndef check_bytes(x: bytes) -> None:\n    pass\n\n\ndef check_list_int(x: list[int]) -> None:\n    pass\n\n\ndef check_list_str(x: list[str]) -> None:\n    pass\n\n\ndef check_tuple_int(x: tuple[int, ...]) -> None:\n    pass\n\n\ndef check_dict_str_int(x: dict[str, int]) -> None:\n    pass\n\n\ndef check_set_int(x: set[int]) -> None:\n    pass\n\n\ndef check_frozenset_int(x: frozenset[int]) -> None:\n    pass\n\n\n# === Value getter functions ===\n\n\ndef get_int() -> int:\n    return 123\n\n\ndef get_float() -> float:\n    return 3.14\n\n\ndef get_str() -> str:\n    return 'hello'\n\n\ndef get_list_int() -> list[int]:\n    return [1, 2, 3]\n\n\ndef get_list_str() -> list[str]:\n    return ['a', 'b', 'c']\n\n\ndef get_object() -> object:\n    return object()\n\n\ndef get_dict_str_int() -> dict[str, int]:\n    return {'a': 1, 'b': 2}\n\n\ndef get_set_str() -> set[str]:\n    return {'a', 'b', 'c'}\n\n\ndef get_frozenset_str() -> frozenset[str]:\n    return frozenset({'a', 'b', 'c'})\n\n\ndef get_tuple_str_int() -> tuple[str, int]:\n    return ('hello', 42)\n\n\ndef get_bytes() -> bytes:\n    return b'hello'\n\n\n# === Core Types ===\n\nobj = object()\nt = type(42)\n\n\n# === Primitive Types ===\n\n# bool\ncheck_bool(True)\ncheck_bool(False)\ncheck_bool(bool(1))\ncheck_bool(bool(''))\n\n# int\ncheck_int(42)\ncheck_int(int('42'))\ncheck_int(int(3.14))\ncheck_int(int(get_int()))\ncheck_int(int(get_float()))\n\n# float\ncheck_float(3.14)\ncheck_float(float('3.14'))\ncheck_float(float(42))\nf = get_float()\nassert_type(f, float)\n\n\n# === String and Bytes Types ===\n\n# str\ncheck_str('hello')\ncheck_str(str(42))\ncheck_str(str(b'hello', 'utf-8'))\ncheck_str(str(get_int()))\n\n# bytes\ncheck_bytes(b'hello')\ncheck_bytes(bytes('hello', 'utf-8'))\ncheck_bytes(bytes(10))\ncheck_bytes(bytes([65, 66, 67]))\ncheck_bytes(bytes(get_int()))\nk2 = get_bytes()\nassert_type(k2, bytes)\n\n\n# === Container Types ===\n\n# list\ncheck_list_int([1, 2, 3])\ncheck_list_str(list('abc'))\ncheck_list_int(list(range(10)))\nm2 = get_list_int()\nassert_type(m2, list[int])\nm3 = get_list_str()\nassert_type(m3, list[str])\n\n# tuple\ncheck_tuple_int(tuple([1, 2, 3]))\np2 = get_tuple_str_int()\nassert_type(p2, tuple[str, int])\n\n# dict\ncheck_dict_str_int({'a': 1, 'b': 2})\ncheck_dict_str_int(dict(a=1, b=2))\nd = get_dict_str_int()\nassert_type(d, dict[str, int])\n\n# set\ncheck_set_int({1, 2, 3})\ncheck_set_int(set([1, 2, 3]))\n\n# frozenset\ncheck_frozenset_int(frozenset([1, 2, 3]))\n\n# range\nw = range(get_int())\nassert_type(w, range)\n\n# slice\nsl1 = slice(10)\nsl2 = slice(0, 10)\nsl3 = slice(0, 10, 2)\n\n\n# === Builtin Functions ===\n\n# abs\ncheck_int(abs(-5))\ncheck_float(abs(-3.14))\n\n# all / any\ncheck_bool(all([True, False]))\ncheck_bool(any([True, False]))\naa = all(get_list_int())\nassert_type(aa, bool)\n\n# bin / hex / oct\ncheck_str(bin(42))\ncheck_str(hex(255))\ncheck_str(oct(8))\nac = bin(get_int())\nassert_type(ac, str)\n\n# chr / ord\ncheck_str(chr(65))\ncheck_int(ord('A'))\naf = chr(get_int())\nassert_type(af, str)\nag = ord(get_str())\nassert_type(ag, int)\n\n# divmod\ndm = divmod(10, 3)\n\n# hash\ncheck_int(hash('hello'))\nai = hash(get_str())\nassert_type(ai, int)\n\n# id\ncheck_int(id(object()))\nak = id(get_object())\nassert_type(ak, int)\n\n# isinstance\ncheck_bool(isinstance(42, int))\nal = isinstance(get_object(), int)\nassert_type(al, bool)\n\n# len\ncheck_int(len([1, 2, 3]))\nan = len(get_list_int())\nassert_type(an, int)\n\n# max / min\ncheck_int(max(1, 2, 3))\ncheck_int(min(1, 2, 3))\n\n# pow\ncheck_int(pow(2, 3))\ncheck_float(pow(2.0, 3.0))\n\n# print\naw = print(get_str())\nassert_type(aw, None)\n\n# repr\ncheck_str(repr(42))\nax = repr(get_int())\nassert_type(ax, str)\n\n# round\ncheck_int(round(3.7))\n\n# sorted\ncheck_list_int(sorted([3, 1, 2]))\n\n# sum\ncheck_int(sum([1, 2, 3]))\nba = sum(get_list_int())\nassert_type(ba, int)\n\n# type\nbf = type(get_int())\nassert_type(bf, type[int])\n\n\n# === Iterator Types ===\n\n# enumerate\nfor i_enum, v_enum in enumerate([1, 2, 3]):\n    check_int(i_enum)\n    check_int(v_enum)\n\n# reversed\nfor v_rev in reversed([1, 2, 3]):\n    check_int(v_rev)\n\n# zip\nfor a_zip, b_zip in zip([1, 2], ['a', 'b']):\n    check_int(a_zip)\n    check_str(b_zip)\n\n\n# === Literal Types ===\n\nbk = None\nassert_type(bk, None)\n\n\n# === Exception Types ===\n\ne1 = BaseException('error')\ne2 = Exception('error')\ne3 = SystemExit(1)\ne4 = KeyboardInterrupt()\ne5 = ArithmeticError('error')\ne6 = OverflowError('error')\ne7 = ZeroDivisionError('error')\ne8 = LookupError('error')\ne9 = IndexError('error')\ne10 = KeyError('key')\ne11 = RuntimeError('error')\ne12 = NotImplementedError('error')\ne13 = RecursionError('error')\ne14 = AttributeError('error')\ne15 = AssertionError('error')\ne16 = MemoryError('error')\ne17 = NameError('error')\ne18 = SyntaxError('error')\ne19 = TimeoutError('error')\ne20 = TypeError('error')\ne21 = ValueError('error')\ne22 = StopIteration()\n\n\n# === Exception Inheritance ===\n\n\ndef handle_base(e: BaseException) -> None:\n    pass\n\n\nhandle_base(Exception('error'))\nhandle_base(ValueError('error'))\nhandle_base(KeyError('key'))\nhandle_base(ZeroDivisionError('error'))\n\n\ndef handle_exception(e: Exception) -> None:\n    pass\n\n\nhandle_exception(ValueError('error'))\nhandle_exception(TypeError('error'))\nhandle_exception(RuntimeError('error'))\n\n\n# === Try/Except ===\n\ntry:\n    x_try = 1 / 0\nexcept ZeroDivisionError as e_try:\n    check_str(str(e_try))\n\ntry:\n    d_try: dict[str, int] = {}\n    v_try = d_try['missing']\nexcept KeyError:\n    pass\n\ntry:\n    lst_try: list[int] = []\n    v_lst = lst_try[0]\nexcept IndexError:\n    pass\n\n\n# === Raise ===\n\n\ndef may_fail(x_fail: int) -> int:\n    if x_fail < 0:\n        raise ValueError('x must be non-negative')\n    if x_fail == 0:\n        raise ZeroDivisionError('x cannot be zero')\n    return 100 // x_fail\n\n\ndef not_implemented() -> None:\n    raise NotImplementedError('subclass must implement')\n\n\nprint(sys.version)\nprint(sys.version_info)\nprint(None, file=sys.stdout)\nprint(None, file=sys.stderr)\n\n# === async ===\n\n\nasync def foo(a: int):\n    return a * 2\n\n\nasync def bar():\n    await foo(1)\n    await foo(2)\n    await foo(3)\n\n\nawait asyncio.gather(bar())  # pyright: ignore\n\nasyncio.run(foo(1))\n\n\n@dataclass\nclass Point:\n    x: int\n    y: float\n\n\np = Point(1, 2)\nassert_type(p.x, int)\nassert_type(p.y, float)\np.x = 3\nprint(p)\n\npath = Path(__file__)\nassert_type(path, Path)\n# assert_type(path.name, str)\n\np2 = path.parent\nassert_type(p2, Path)\n\np3 = path / 'test.txt'\nassert_type(p3, Path)\nassert p3.name == 'test.txt'\n\nx = os.getenv('foobar')\nassert_type(x, str | None)\n\ny = os.getenv('foobar', default=int('123'))\nassert_type(y, str | int)\n\nx2 = os.environ.get('foobar')\nassert_type(x2, str | None)\n\n\n# === re module ===\n\n# re.search returns Match or None\ns1 = re.search(r'\\d+', 'abc 42')\nassert_type(s1, re.Match[str] | None)\n\n# re.match returns Match or None\ns2 = re.match(r'\\w+', 'hello')\nassert_type(s2, re.Match[str] | None)\n\n# re.fullmatch returns Match or None\ns3 = re.fullmatch(r'\\w+', 'hello')\nassert_type(s3, re.Match[str] | None)\n\n# re.compile returns Pattern\np_re = re.compile(r'\\d+')\nassert_type(p_re, re.Pattern[str])\n\n# re.findall returns list of Any\nfa = re.findall(r'\\d+', 'a1 b2 c3')\nassert_type(fa, list[Any])\n\n# re.sub returns str\ns4 = re.sub(r'\\d+', 'X', 'a1 b2')\nassert_type(s4, str)\n\n# Pattern.search returns Match or None\np_re2 = re.compile(r'(\\w+)')\ns5 = p_re2.search('hello world')\nassert_type(s5, re.Match[str] | None)\n\n# Pattern.match returns Match or None\ns6 = p_re2.match('hello world')\nassert_type(s6, re.Match[str] | None)\n\n# Pattern.sub returns str\ns7 = p_re2.sub('X', 'hello world')\nassert_type(s7, str)\n\n# Pattern.findall returns list of Any\nfa2 = p_re2.findall('hello world')\nassert_type(fa2, list[Any])\n"
  },
  {
    "path": "crates/monty-type-checking/tests/main.rs",
    "content": "use std::fs;\n\nuse monty_type_checking::{SourceFile, type_check};\nuse pretty_assertions::assert_eq;\nuse ruff_db::diagnostic::DiagnosticFormat;\n\n#[test]\nfn type_checking_success() {\n    let code = r\"\ndef add(x: int, y: int) -> int:\n    return x + y\n\nresult = add(1, 2)\n    \";\n\n    let result = type_check(&SourceFile::new(code, \"main.py\"), None).unwrap();\n    assert!(result.is_none());\n}\n\n#[test]\nfn type_checking_error() {\n    let code = \"\\\ndef add(x: int, y: int) -> int:\n    return x + y\n\nresult = add(1, '2')\n    \";\n\n    let result = type_check(&SourceFile::new(code, \"main.py\"), None).unwrap();\n    assert!(result.is_some());\n\n    let error_diagnostics = result.unwrap().to_string();\n    assert_eq!(\n        error_diagnostics,\n        r#\"error[invalid-argument-type]: Argument to function `add` is incorrect\n --> main.py:4:17\n  |\n2 |     return x + y\n3 |\n4 | result = add(1, '2')\n  |                 ^^^ Expected `int`, found `Literal[\"2\"]`\n  |\ninfo: Function defined here\n --> main.py:1:5\n  |\n1 | def add(x: int, y: int) -> int:\n  |     ^^^         ------ Parameter declared here\n2 |     return x + y\n  |\ninfo: rule `invalid-argument-type` is enabled by default\n\n\"#\n    );\n}\n\n#[test]\nfn type_checking_error_stubs() {\n    let stubs = \"\\\nfrom dataclasses import dataclass\n\n@dataclass\nclass User:\n    name: str\n    age: int\n\";\n    let code = \"\\\ndef add(x: int, y: int) -> int:\n    return x + y\n\nresult = add(1, '2')\";\n\n    let result = type_check(\n        &SourceFile::new(code, \"main.py\"),\n        Some(&SourceFile::new(stubs, \"type_stubs.pyi\")),\n    )\n    .unwrap();\n\n    let error_diagnostics = result.unwrap();\n    assert_eq!(\n        error_diagnostics.to_string(),\n        r#\"error[invalid-argument-type]: Argument to function `add` is incorrect\n --> main.py:4:17\n  |\n2 |     return x + y\n3 |\n4 | result = add(1, '2')\n  |                 ^^^ Expected `int`, found `Literal[\"2\"]`\n  |\ninfo: Function defined here\n --> main.py:1:5\n  |\n1 | def add(x: int, y: int) -> int:\n  |     ^^^         ------ Parameter declared here\n2 |     return x + y\n  |\ninfo: rule `invalid-argument-type` is enabled by default\n\n\"#\n    );\n}\n\n#[test]\nfn type_checking_error_concise() {\n    let code = r\"\ndef add(x: int, y: int) -> int:\n    return x + y\n\nresult = add(1, '2')\n    \";\n\n    let result = type_check(&SourceFile::new(code, \"main.py\"), None).unwrap();\n    assert!(result.is_some());\n\n    let failure = result.unwrap().format(DiagnosticFormat::Concise);\n    let error_diagnostics = failure.to_string();\n    assert_eq!(\n        error_diagnostics,\n        \"main.py:5:17: error[invalid-argument-type] Argument to function `add` is incorrect: Expected `int`, found `Literal[\\\"2\\\"]`\\n\"\n    );\n    let color_failure = failure.color(true).to_string();\n    assert!(color_failure.starts_with('\\u{1b}'));\n}\n\n#[test]\nfn missing_stdlib_datetime() {\n    let code = \"import datetime\\nprint(datetime.datetime.now())\";\n\n    let result = type_check(&SourceFile::new(code, \"main.py\"), None).unwrap();\n    assert!(result.is_some());\n\n    let failure = result.unwrap().format(DiagnosticFormat::Concise);\n    let error_diagnostics = failure.to_string();\n    assert_eq!(\n        error_diagnostics,\n        \"main.py:1:8: error[unresolved-import] Cannot resolve imported module `datetime`\\n\"\n    );\n    let dbg = format!(\"{failure:?}\");\n    assert!(dbg.starts_with(\"TypeCheckingDiagnostics:\"), \"got: {dbg}\");\n}\n\n/// Test that good_types.py type-checks without errors.\n///\n/// This file uses `assert_type` from typing to verify that inferred types match expected types.\n#[test]\nfn type_good_types() {\n    let code = include_str!(\"good_types.py\");\n    let result = type_check(&SourceFile::new(code, \"good_types.py\"), None).unwrap();\n    assert!(result.is_none(), \"Expected no type errors, got: {result:#?}\");\n}\n\nfn check_file_content(file_name: &str, mut actual: &str) {\n    let expected_path = format!(\"{}/tests/{}\", env!(\"CARGO_MANIFEST_DIR\"), file_name);\n    let expected = if fs::exists(&expected_path).unwrap() {\n        fs::read_to_string(&expected_path).unwrap()\n    } else {\n        std::fs::write(&expected_path, actual).unwrap();\n        panic!(\"{file_name} did not exist, file created.\")\n    };\n\n    let expected = expected.as_str().trim();\n    actual = actual.trim();\n\n    if actual == expected {\n        println!(\"File content matches expected.\");\n        return;\n    }\n\n    let status = if std::env::var(\"UPDATE_EXPECT\").is_ok() {\n        std::fs::write(&expected_path, actual).unwrap();\n        \"FILE UPDATE\"\n    } else {\n        \"file not updated, run with UPDATE_EXPECT=1 to update\"\n    };\n\n    panic!(\"Type errors don't match expected.\\n\\nEXPECTED:\\n{expected}\\n\\nACTUAL:\\n{actual}\\n\\n{status}.\");\n}\n\n/// Test that bad_types.py produces the expected type errors.\n///\n/// Set `UPDATE_EXPECT=1` to update the expected errors file.\n#[test]\nfn type_bad_types() {\n    let code = include_str!(\"bad_types.py\");\n    let result = type_check(&SourceFile::new(code, \"bad_types.py\"), None).unwrap();\n\n    let failure = result.expect(\"Expected type errors in bad_types.py\");\n    let actual = failure\n        .format(ruff_db::diagnostic::DiagnosticFormat::Concise)\n        .to_string();\n\n    check_file_content(\"bad_types_output.txt\", &actual);\n}\n\n#[test]\nfn test_reveal_types() {\n    let code = include_str!(\"reveal_types.py\");\n    let result = type_check(&SourceFile::new(code, \"reveal_types.py\"), None).unwrap();\n\n    let failure = result.expect(\"Expected type errors in reveal_types.py\");\n    let actual = failure\n        .format(ruff_db::diagnostic::DiagnosticFormat::Concise)\n        .to_string();\n\n    check_file_content(\"reveal_types_output.txt\", &actual);\n}\n"
  },
  {
    "path": "crates/monty-type-checking/tests/reveal_types.py",
    "content": "from typing import reveal_type\n\n# === Core types ===\nreveal_type(None)\nreveal_type(object())\nreveal_type(type(1))\n\n# === Primitive types ===\nreveal_type(True)\nreveal_type(int(1))\nreveal_type(float(1.2))\n\n# === String/bytes types ===\nreveal_type('hello')\nreveal_type(b'foobar')\n\n# === Container types ===\nreveal_type([1])\nreveal_type((1, 2))\nreveal_type({1: 2})\nreveal_type({1, 2})\nreveal_type(frozenset({1, 2}))\nreveal_type(range(10))\n\n# === Iterator types ===\nreveal_type(enumerate([1, 2]))\nreveal_type(reversed([1, 2]))\nreveal_type(zip([1], [2]))\n\n# === Slicing ===\nreveal_type(slice(1, 2))\n\n# === Exception types ===\nreveal_type(BaseException())\nreveal_type(Exception())\nreveal_type(SystemExit())\nreveal_type(KeyboardInterrupt())\nreveal_type(ArithmeticError())\nreveal_type(OverflowError())\nreveal_type(ZeroDivisionError())\nreveal_type(LookupError())\nreveal_type(IndexError())\nreveal_type(KeyError())\nreveal_type(RuntimeError())\nreveal_type(NotImplementedError())\nreveal_type(RecursionError())\nreveal_type(AttributeError())\nreveal_type(AssertionError())\nreveal_type(MemoryError())\nreveal_type(NameError())\nreveal_type(SyntaxError())\nreveal_type(OSError())\nreveal_type(TimeoutError())\nreveal_type(TypeError())\nreveal_type(ValueError())\nreveal_type(StopIteration())\n"
  },
  {
    "path": "crates/monty-type-checking/tests/reveal_types_output.txt",
    "content": "reveal_types.py:4:13: info[revealed-type] Revealed type: `None`\nreveal_types.py:5:13: info[revealed-type] Revealed type: `object`\nreveal_types.py:6:13: info[revealed-type] Revealed type: `<class 'int'>`\nreveal_types.py:9:13: info[revealed-type] Revealed type: `Literal[True]`\nreveal_types.py:10:13: info[revealed-type] Revealed type: `int`\nreveal_types.py:11:13: info[revealed-type] Revealed type: `float`\nreveal_types.py:14:13: info[revealed-type] Revealed type: `Literal[\"hello\"]`\nreveal_types.py:15:13: info[revealed-type] Revealed type: `Literal[b\"foobar\"]`\nreveal_types.py:18:13: info[revealed-type] Revealed type: `list[Unknown | int]`\nreveal_types.py:19:13: info[revealed-type] Revealed type: `tuple[Literal[1], Literal[2]]`\nreveal_types.py:20:13: info[revealed-type] Revealed type: `dict[Unknown | int, Unknown | int]`\nreveal_types.py:21:13: info[revealed-type] Revealed type: `set[Unknown | int]`\nreveal_types.py:22:13: info[revealed-type] Revealed type: `frozenset[int]`\nreveal_types.py:23:13: info[revealed-type] Revealed type: `range`\nreveal_types.py:26:13: info[revealed-type] Revealed type: `enumerate[int]`\nreveal_types.py:27:13: info[revealed-type] Revealed type: `reversed[int]`\nreveal_types.py:28:13: info[revealed-type] Revealed type: `zip[Unknown]`\nreveal_types.py:31:13: info[revealed-type] Revealed type: `slice[Any, Any, Any]`\nreveal_types.py:34:13: info[revealed-type] Revealed type: `BaseException`\nreveal_types.py:35:13: info[revealed-type] Revealed type: `Exception`\nreveal_types.py:36:13: info[revealed-type] Revealed type: `SystemExit`\nreveal_types.py:37:13: info[revealed-type] Revealed type: `KeyboardInterrupt`\nreveal_types.py:38:13: info[revealed-type] Revealed type: `ArithmeticError`\nreveal_types.py:39:13: info[revealed-type] Revealed type: `OverflowError`\nreveal_types.py:40:13: info[revealed-type] Revealed type: `ZeroDivisionError`\nreveal_types.py:41:13: info[revealed-type] Revealed type: `LookupError`\nreveal_types.py:42:13: info[revealed-type] Revealed type: `IndexError`\nreveal_types.py:43:13: info[revealed-type] Revealed type: `KeyError`\nreveal_types.py:44:13: info[revealed-type] Revealed type: `RuntimeError`\nreveal_types.py:45:13: info[revealed-type] Revealed type: `NotImplementedError`\nreveal_types.py:46:13: info[revealed-type] Revealed type: `RecursionError`\nreveal_types.py:47:13: info[revealed-type] Revealed type: `AttributeError`\nreveal_types.py:48:13: info[revealed-type] Revealed type: `AssertionError`\nreveal_types.py:49:13: info[revealed-type] Revealed type: `MemoryError`\nreveal_types.py:50:13: info[revealed-type] Revealed type: `NameError`\nreveal_types.py:51:13: info[revealed-type] Revealed type: `SyntaxError`\nreveal_types.py:52:13: info[revealed-type] Revealed type: `OSError`\nreveal_types.py:53:13: info[revealed-type] Revealed type: `TimeoutError`\nreveal_types.py:54:13: info[revealed-type] Revealed type: `TypeError`\nreveal_types.py:55:13: info[revealed-type] Revealed type: `ValueError`\nreveal_types.py:56:13: info[revealed-type] Revealed type: `StopIteration`\n"
  },
  {
    "path": "crates/monty-typeshed/.gitignore",
    "content": "# Do not ignore any of the vendored files. If this pattern is not present,\n# we will gitignore the `venv/` stubs in typeshed, as there is a general\n# rule to ignore `venv/` directories in the root `.gitignore`.\n!/vendor/typeshed/**/*\n/typeshed-repo/\n"
  },
  {
    "path": "crates/monty-typeshed/Cargo.toml",
    "content": "[package]\nname = \"monty_typeshed\"\nreadme = \"README.md\"\nversion = { workspace = true }\nrust-version = { workspace = true }\nedition = { workspace = true }\nauthors = { workspace = true }\nlicense = { workspace = true }\ndescription = { workspace = true }\nkeywords = { workspace = true }\ncategories = { workspace = true }\nhomepage = { workspace = true }\nrepository = { workspace = true }\n\n[dependencies]\nruff_db = { workspace = true }\nzip = { version = \"0.6.6\", default-features = false }\n\n[build-dependencies]\npath-slash = \"0.2.1\"\nwalkdir = \"2.3.2\"\nzip = { version = \"0.6.6\", features = [\"zstd\", \"deflate\"] }\n\n[features]\nzstd = [\"zip/zstd\"]\ndeflate = [\"zip/deflate\"]\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/monty-typeshed/README.md",
    "content": "# Vendored types for a very minimal subset of the CPython stdlib\n\nCopied originally from <https://github.com/astral-sh/ruff/tree/main/crates/ty_vendored> but only parts of\n<https://github.com/python/typeshed/blob/main/stdlib/builtins.pyi> are kept, since those are the\nonly functions supported from the stdlib.\n\nThe `vendor/typeshed` directory is updated by calling `make update-typeshed` which calls the `update.py` script in this directory.\n\nSee <https://github.com/pydantic/monty> for more information on the project.\n\nTHEREFORE FILES IN THE `vendor/typeshed` DIRECTORY SHOULD NOT BE EDITED MANUALLY.\n"
  },
  {
    "path": "crates/monty-typeshed/build.rs",
    "content": "//! Build script to package our vendored typeshed files\n//! into a zip archive that can be included in the Ruff binary.\n//!\n//! This script should be automatically run at build time\n//! whenever the script itself changes, or whenever any files\n//! in `crates/ty_vendored/vendor/typeshed` change.\n#![expect(clippy::unnecessary_debug_formatting)]\n\nuse std::{fs::File, io::Write, path::Path};\n\nuse path_slash::PathExt;\nuse zip::{\n    CompressionMethod,\n    result::ZipResult,\n    write::{FileOptions, ZipWriter},\n};\n\nconst TYPESHED_SOURCE_DIR: &str = \"vendor/typeshed\";\n// const TY_EXTENSIONS_STUBS: &str = \"ty_extensions/ty_extensions.pyi\";\nconst TYPESHED_ZIP_LOCATION: &str = \"/zipped_typeshed.zip\";\n\n/// Recursively zip the contents of the entire typeshed directory and patch typeshed\n/// on the fly to include the `ty_extensions` module.\n///\n/// This routine is adapted from a recipe at\n/// <https://github.com/zip-rs/zip-old/blob/5d0f198124946b7be4e5969719a7f29f363118cd/examples/write_dir.rs>\nfn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {\n    let mut zip = ZipWriter::new(writer);\n\n    // Use deflated compression for WASM builds because compiling `zstd-sys` requires clang\n    // [source](https://github.com/gyscos/zstd-rs/wiki/Compile-for-WASM) which complicates the build\n    // by a lot. Deflated compression is slower but it shouldn't matter much for the WASM use case\n    // (WASM itself is already slower than a native build for a specific platform).\n    // We can't use `#[cfg(...)]` here because the target-arch in a build script is the\n    // architecture of the system running the build script and not the architecture of the build-target.\n    // That's why we use the `TARGET` environment variable here.\n    let method = if cfg!(feature = \"zstd\") {\n        CompressionMethod::Zstd\n    } else if cfg!(feature = \"deflate\") {\n        CompressionMethod::Deflated\n    } else {\n        CompressionMethod::Stored\n    };\n\n    let options = FileOptions::default()\n        .compression_method(method)\n        .unix_permissions(0o644);\n\n    for entry in walkdir::WalkDir::new(TYPESHED_SOURCE_DIR) {\n        let dir_entry = entry.unwrap();\n        let absolute_path = dir_entry.path();\n        let normalized_relative_path = absolute_path\n            .strip_prefix(Path::new(TYPESHED_SOURCE_DIR))\n            .unwrap()\n            .to_slash()\n            .expect(\"Unexpected non-utf8 typeshed path!\");\n\n        // Write file or directory explicitly\n        // Some unzip tools unzip files with directory paths correctly, some do not!\n        if absolute_path.is_file() {\n            println!(\"adding file {absolute_path:?} as {normalized_relative_path:?} ...\");\n            zip.start_file(&*normalized_relative_path, options)?;\n            let mut f = File::open(absolute_path)?;\n            std::io::copy(&mut f, &mut zip).unwrap();\n\n            // Patch the VERSIONS file to make `ty_extensions` available\n            if normalized_relative_path == \"stdlib/VERSIONS\" {\n                writeln!(&mut zip, \"ty_extensions: 3.0-\")?;\n            }\n        } else if !normalized_relative_path.is_empty() {\n            // Only if not root! Avoids path spec / warning\n            // and mapname conversion failed error on unzip\n            println!(\"adding dir {absolute_path:?} as {normalized_relative_path:?} ...\");\n            zip.add_directory(normalized_relative_path, options)?;\n        }\n    }\n\n    // // Patch typeshed and add the stubs for the `ty_extensions` module\n    // println!(\"adding file {TY_EXTENSIONS_STUBS} as stdlib/ty_extensions.pyi ...\");\n    // zip.start_file(\"stdlib/ty_extensions.pyi\", options)?;\n    // let mut f = File::open(TY_EXTENSIONS_STUBS)?;\n    // std::io::copy(&mut f, &mut zip).unwrap();\n\n    zip.finish()\n}\n\nfn main() {\n    assert!(Path::new(TYPESHED_SOURCE_DIR).is_dir(), \"Where is typeshed?\");\n    let out_dir = std::env::var(\"OUT_DIR\").unwrap();\n\n    // N.B. Deliberately using `format!()` instead of `Path::join()` here,\n    // so that we use `/` as a path separator on all platforms.\n    // That enables us to load the typeshed zip at compile time in `module.rs`\n    // (otherwise we'd have to dynamically determine the exact path to the typeshed zip\n    // based on the default path separator for the specific platform we're on,\n    // which can't be done at compile time.)\n    let zipped_typeshed_location = format!(\"{out_dir}{TYPESHED_ZIP_LOCATION}\");\n\n    let zipped_typeshed_file = File::create(zipped_typeshed_location).unwrap();\n    write_zipped_typeshed_to(zipped_typeshed_file).unwrap();\n}\n"
  },
  {
    "path": "crates/monty-typeshed/custom/README.md",
    "content": "This directory contains custom type stubs where types available in monty differ from those in the standard library.\n"
  },
  {
    "path": "crates/monty-typeshed/custom/asyncio.pyi",
    "content": "from collections.abc import Awaitable, Generator\nfrom typing import Any, Literal, TypeAlias, TypeVar, overload\n\n_T = TypeVar('_T')\n_T1 = TypeVar('_T1')\n_T2 = TypeVar('_T2')\n_T3 = TypeVar('_T3')\n_T4 = TypeVar('_T4')\n_T5 = TypeVar('_T5')\n_T6 = TypeVar('_T6')\n\nclass _Future(Awaitable[_T]):\n    \"\"\"\n    Minimal copy of Future from _typeshed/stdlib/_asyncio.pyi\n    \"\"\"\n    def __iter__(self) -> Generator[Any, None, _T]: ...\n    def __await__(self) -> Generator[Any, None, _T]: ...\n\n_FutureLike: TypeAlias = _Future[_T] | Awaitable[_T]\n\ndef run(main: Awaitable[_T], *, debug: bool | None = None, loop_factory: Any = None) -> _T: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: Literal[False] = False\n) -> _Future[tuple[_T1]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3, _T4]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3, _T4, _T5]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    coro_or_future6: _FutureLike[_T6],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3, _T4, _T5, _T6]]: ...\n@overload\ndef gather(*coros_or_futures: _FutureLike[_T], return_exceptions: Literal[False] = False) -> _Future[list[_T]]: ...\n@overload\ndef gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: bool) -> _Future[tuple[_T1 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: bool\n) -> _Future[tuple[_T1 | BaseException, _T2 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[\n    tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException, _T5 | BaseException]\n]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    coro_or_future6: _FutureLike[_T6],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[\n    tuple[\n        _T1 | BaseException,\n        _T2 | BaseException,\n        _T3 | BaseException,\n        _T4 | BaseException,\n        _T5 | BaseException,\n        _T6 | BaseException,\n    ]\n]: ...\n@overload\ndef gather(*coros_or_futures: _FutureLike[_T], return_exceptions: bool) -> _Future[list[_T | BaseException]]: ...\n"
  },
  {
    "path": "crates/monty-typeshed/custom/os.pyi",
    "content": "from abc import ABC, abstractmethod\nfrom typing import Callable, Protocol, TypeAlias, TypeVar, final, overload, runtime_checkable\n\nfrom _typeshed import AnyStr_co, structseq\n\n_T = TypeVar('_T')\nenviron: dict[str, str]\n\n@overload\ndef getenv(key: str) -> str | None: ...\n@overload\ndef getenv(key: str, default: _T) -> str | _T: ...\n@final\nclass stat_result(structseq[float], tuple[int, int, int, int, int, int, int, float, float, float]):\n    # The constructor of this class takes an iterable of variable length (though it must be at least 10).\n    #\n    # However, this class behaves like a tuple of 10 elements,\n    # no matter how long the iterable supplied to the constructor is.\n    # https://github.com/python/typeshed/pull/6560#discussion_r767162532\n    #\n    # The 10 elements always present are st_mode, st_ino, st_dev, st_nlink,\n    # st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime.\n    #\n    # More items may be added at the end by some implementations.\n\n    @property\n    def st_mode(self) -> int:\n        \"\"\"protection bits\"\"\"\n        ...\n\n    @property\n    def st_ino(self) -> int:\n        \"\"\"inode\"\"\"\n        ...\n\n    @property\n    def st_dev(self) -> int:\n        \"\"\"device\"\"\"\n        ...\n\n    @property\n    def st_nlink(self) -> int:\n        \"\"\"number of hard links\"\"\"\n        ...\n\n    @property\n    def st_uid(self) -> int:\n        \"\"\"user ID of owner\"\"\"\n        ...\n\n    @property\n    def st_gid(self) -> int:\n        \"\"\"group ID of owner\"\"\"\n        ...\n\n    @property\n    def st_size(self) -> int:\n        \"\"\"total size, in bytes\"\"\"\n        ...\n\n    @property\n    def st_atime(self) -> float:\n        \"\"\"time of last access\"\"\"\n        ...\n\n    @property\n    def st_mtime(self) -> float:\n        \"\"\"time of last modification\"\"\"\n        ...\n\n    @property\n    def st_ctime(self) -> float:\n        \"\"\"time of last change\"\"\"\n        ...\n\n# (Samuel) PathLike is included here because it's used by pathlib\n\n# mypy and pyright object to this being both ABC and Protocol.\n# At runtime it inherits from ABC and is not a Protocol, but it will be\n# on the allowlist for use as a Protocol starting in 3.14.\n@runtime_checkable\nclass PathLike(ABC, Protocol[AnyStr_co]):  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n    __slots__ = ()\n    @abstractmethod\n    def __fspath__(self) -> AnyStr_co: ...\n\n_Opener: TypeAlias = Callable[[str, int], int]\n"
  },
  {
    "path": "crates/monty-typeshed/custom/sys.pyi",
    "content": "from typing import Any, Final, Literal, TextIO, final, type_check_only\n\nfrom _typeshed import MaybeNone, structseq\nfrom typing_extensions import TypeAlias\n\n# stdin: TextIO | MaybeNone\nstdout: TextIO | MaybeNone\nstderr: TextIO | MaybeNone\n\nversion: str\n\n# Type alias used as a mixin for structseq classes that cannot be instantiated at runtime\n# This can't be represented in the type system, so we just use `structseq[Any]`\n_UninstantiableStructseq: TypeAlias = structseq[Any]\n_ReleaseLevel: TypeAlias = Literal['alpha', 'beta', 'candidate', 'final']\n\n@final\n@type_check_only\nclass _version_info(_UninstantiableStructseq, tuple[int, int, int, _ReleaseLevel, int]):\n    __match_args__: Final = ('major', 'minor', 'micro', 'releaselevel', 'serial')\n\n    @property\n    def major(self) -> int: ...\n    @property\n    def minor(self) -> int: ...\n    @property\n    def micro(self) -> int: ...\n    @property\n    def releaselevel(self) -> _ReleaseLevel: ...\n    @property\n    def serial(self) -> int: ...\n\nversion_info: _version_info\n"
  },
  {
    "path": "crates/monty-typeshed/src/lib.rs",
    "content": "use std::sync::LazyLock;\n\nuse ruff_db::vendored::VendoredFileSystem;\n\n/// The source commit of the vendored typeshed.\npub const SOURCE_COMMIT: &str = include_str!(\"../vendor/typeshed/source_commit.txt\").trim_ascii_end();\n\n// The file path here is hardcoded in this crate's `build.rs` script.\n// Luckily this crate will fail to build if this file isn't available at build time.\nstatic TYPESHED_ZIP_BYTES: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/zipped_typeshed.zip\"));\n\n#[must_use]\npub fn file_system() -> &'static VendoredFileSystem {\n    static VENDORED_TYPESHED_STUBS: LazyLock<VendoredFileSystem> =\n        LazyLock::new(|| VendoredFileSystem::new_static(TYPESHED_ZIP_BYTES).unwrap());\n    &VENDORED_TYPESHED_STUBS\n}\n\n#[cfg(test)]\nmod tests {\n    use std::io::{self, Read};\n\n    use super::*;\n\n    #[test]\n    fn test_commit() {\n        assert_eq!(SOURCE_COMMIT.len(), 40);\n    }\n\n    #[test]\n    fn typeshed_zip_created_at_build_time() {\n        let mut typeshed_zip_archive = zip::ZipArchive::new(io::Cursor::new(TYPESHED_ZIP_BYTES)).unwrap();\n\n        let mut builtins_stub = typeshed_zip_archive.by_name(\"stdlib/builtins.pyi\").unwrap();\n        assert!(builtins_stub.is_file());\n\n        let mut builtins_source = String::new();\n        builtins_stub.read_to_string(&mut builtins_source).unwrap();\n\n        assert!(builtins_source.contains(\"class int:\"));\n    }\n\n    #[test]\n    fn typeshed_versions_file_exists() {\n        let mut typeshed_zip_archive = zip::ZipArchive::new(io::Cursor::new(TYPESHED_ZIP_BYTES)).unwrap();\n\n        let mut versions_file = typeshed_zip_archive.by_name(\"stdlib/VERSIONS\").unwrap();\n        assert!(versions_file.is_file());\n\n        let mut versions_content = String::new();\n        versions_file.read_to_string(&mut versions_content).unwrap();\n\n        // VERSIONS file should contain module version info like \"builtins: 3.0-\"\n        assert!(versions_content.contains(\"builtins:\"));\n    }\n}\n"
  },
  {
    "path": "crates/monty-typeshed/update.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Update vendored typeshed files from the upstream repository.\n\nThis script:\n1. Clones the typeshed repository to crates/monty-typeshed/typeshed-repo (or updates if it exists)\n2. Records the HEAD commit hash\n3. Filters builtins.pyi to keep only supported classes and functions\n4. Writes the filtered file to the vendor directory\n\nUsage:\n    python crates/monty-typeshed/update.py\n\"\"\"\n\nimport ast\nimport shutil\nimport subprocess\nfrom pathlib import Path\n\n# Whitelisted builtin functions (from crates/monty/src/builtins/)\nALLOWED_FUNCTIONS = {\n    'abs',\n    'all',\n    'any',\n    'bin',\n    'chr',\n    'divmod',\n    'hash',\n    'hex',\n    'id',\n    'isinstance',\n    'len',\n    'max',\n    'min',\n    'oct',\n    'ord',\n    'pow',\n    'print',\n    'repr',\n    'round',\n    'sorted',\n    'sum',\n}\n\n# Whitelisted builtin classes (from crates/monty/src/types/ and exception_private.rs)\nALLOWED_CLASSES = {\n    # Core types\n    'object',\n    'type',\n    # Primitive types\n    'bool',\n    'int',\n    'float',\n    # String/bytes types\n    'str',\n    'bytes',\n    # Container types\n    'list',\n    'tuple',\n    'dict',\n    'set',\n    'frozenset',\n    'range',\n    # Iterator types (these are classes, not functions)\n    'enumerate',\n    'reversed',\n    'zip',\n    # Slicing\n    'slice',\n    # property is used by pathlib.Path\n    'property',\n    # Exception hierarchy (from crates/monty/src/exception_private.rs)\n    'BaseException',\n    'Exception',\n    'SystemExit',\n    'KeyboardInterrupt',\n    'ArithmeticError',\n    'OverflowError',\n    'ZeroDivisionError',\n    'LookupError',\n    'IndexError',\n    'KeyError',\n    'RuntimeError',\n    'NotImplementedError',\n    'RecursionError',\n    'AttributeError',\n    'AssertionError',\n    'MemoryError',\n    'NameError',\n    'SyntaxError',\n    'OSError',\n    'TimeoutError',\n    'TypeError',\n    'ValueError',\n    'StopIteration',\n}\n\n# Files to copy without filtering\nCOPY_FILES = [\n    # Core type system\n    'typing.pyi',\n    'typing_extensions.pyi',\n    '_collections_abc.pyi',\n    # Used in type annotations\n    'types.pyi',\n    # So type checking works with dataclasses\n    'dataclasses.pyi',\n    # used by dataclasses\n    'enum.pyi',\n    # the re std lib module is not mostly implemented\n    're.pyi',\n    # ==============================\n    # all all collections dir\n    'collections/__init__.pyi',\n    'collections/abc.pyi',\n    # ==============================\n    # Take only `__init__.pyi` from _typeshed dir\n    '_typeshed/__init__.pyi',\n    # ==============================\n    # all of pathlib dir\n    'pathlib/__init__.pyi',\n    'pathlib/types.pyi',\n    # ==============================\n    # math module\n    'math.pyi',\n]\n# content for typeshed's `VERSIONS` file\nVERSIONS = \"\"\"\\\n# DO NOT EDIT THIS FILE DIRECTLY\n# instead edit crates/monty-typeshed/update.py\n# this file should match the modules\n# which monty's minimimal typeshed includes\n\n_collections_abc: 3.3-\n_typeshed: 3.0-  # not present at runtime, only for type checking\nasyncio: 3.4-\nbuiltins: 3.0-\ncollections: 3.0-\ndataclasses: 3.7-\nmath: 3.0-\nos: 3.0-\npathlib: 3.4-\npathlib.types: 3.14-\nre: 3.0-\nsys: 3.0-\ntyping: 3.5-\ntyping_extensions: 3.7-\ntypes: 3.0-\n\"\"\"\n\nCRATE_DIR = Path(__file__).parent\nREPO_ROOT = CRATE_DIR.parent.parent\nVENDOR_DIR = CRATE_DIR / 'vendor' / 'typeshed'\nSTDLIB_DIR = VENDOR_DIR / 'stdlib'\nCUSTOM_DIR = CRATE_DIR / 'custom'\nTYPESHED_REPO_DIR = CRATE_DIR / 'typeshed-repo'\n\nTYPESHED_REPO_URL = 'git@github.com:python/typeshed.git'\n\n\ndef clone_or_update_typeshed() -> str:\n    \"\"\"Clone or update the typeshed repository and return the path and HEAD commit hash.\n\n    If the repository already exists at TYPESHED_REPO_DIR, performs a git pull.\n    Otherwise, clones the repository to that location.\n\n    Returns:\n        commit_hash\n    \"\"\"\n    if TYPESHED_REPO_DIR.exists():\n        print(f'{TYPESHED_REPO_DIR} exists, not pulling')\n    else:\n        print(f'Cloning typeshed to {TYPESHED_REPO_DIR}...')\n        subprocess.run(\n            ['git', 'clone', '--depth=1', TYPESHED_REPO_URL, str(TYPESHED_REPO_DIR)],\n            check=True,\n            capture_output=True,\n        )\n\n    result = subprocess.run(\n        ['git', 'rev-parse', 'HEAD'],\n        cwd=TYPESHED_REPO_DIR,\n        check=True,\n        capture_output=True,\n        text=True,\n    )\n    return result.stdout.strip()\n\n\ndef filter_statements(nodes: list[ast.stmt]) -> list[ast.stmt]:\n    \"\"\"Filter a list of statements to keep only allowed functions and classes.\n\n    Keeps:\n    - Imports\n    - Type variable assignments (e.g., _T = TypeVar('_T'))\n    - Allowed function definitions\n    - Allowed class definitions\n\n    Args:\n        nodes: List of AST statement nodes.\n\n    Returns:\n        Filtered list of statements.\n    \"\"\"\n    result: list[ast.stmt] = []\n    for node in nodes:\n        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n            if node.name in ALLOWED_FUNCTIONS:\n                result.append(node)\n        elif isinstance(node, ast.ClassDef):\n            if node.name.startswith('_') or node.name in ALLOWED_CLASSES:\n                result.append(node)\n        elif isinstance(node, ast.If):\n            # Recursively filter version-conditional blocks\n            filtered = filter_if_block(node)\n            if filtered is not None:\n                result.append(filtered)\n        else:\n            # Keep imports, type aliases, assignments, etc.\n            result.append(node)\n    return result\n\n\ndef filter_if_block(node: ast.If) -> ast.If | None:\n    \"\"\"Filter an if block, recursively filtering function and class definitions.\n\n    Handles version conditionals like `if sys.version_info >= (3, 10):`.\n\n    Args:\n        node: An ast.If node.\n\n    Returns:\n        Filtered If node, or None if both branches are empty after filtering.\n    \"\"\"\n    filtered_body = filter_statements(node.body)\n    filtered_orelse = filter_statements(node.orelse)\n\n    # If both branches are empty, skip this if block entirely\n    if not filtered_body and not filtered_orelse:\n        return None\n\n    # Create a new If node with filtered contents\n    new_node = ast.If(\n        test=node.test,\n        body=filtered_body if filtered_body else [ast.Pass()],\n        orelse=filtered_orelse,\n    )\n    return ast.copy_location(new_node, node)\n\n\ndef filter_builtins(source: str) -> str:\n    \"\"\"Filter builtins.pyi to keep only allowed classes and functions.\n\n    This function parses the source with Python's ast module and filters\n    top-level definitions to only include those in the allow lists.\n    All imports and type definitions are preserved.\n\n    Args:\n        source: The source code of builtins.pyi.\n\n    Returns:\n        Filtered source code.\n    \"\"\"\n    tree = ast.parse(source)\n    tree.body = filter_statements(tree.body)\n    ast.fix_missing_locations(tree)\n    return ast.unparse(tree)\n\n\ndef main() -> int:\n    \"\"\"Main entry point.\"\"\"\n    # Clean up any stale files from previous runs\n    if VENDOR_DIR.exists():\n        print(f'Removing existing {VENDOR_DIR}...')\n        shutil.rmtree(VENDOR_DIR)\n\n    # Clone or update typeshed\n    commit = clone_or_update_typeshed()\n    print(f'At python/typeshed commit {commit}')\n\n    # Read source file\n    src_stdlib = TYPESHED_REPO_DIR / 'stdlib'\n    builtins_path = src_stdlib / 'builtins.pyi'\n    source = builtins_path.read_text()\n\n    # Filter\n    filtered = filter_builtins(source)\n\n    # Write output files\n    STDLIB_DIR.mkdir(parents=True, exist_ok=True)\n\n    (STDLIB_DIR / 'builtins.pyi').write_text(filtered)\n    print(f'Wrote {(STDLIB_DIR / \"builtins.pyi\").relative_to(REPO_ROOT)}')\n\n    (STDLIB_DIR / 'VERSIONS').write_text(VERSIONS)\n    print(f'Wrote {(STDLIB_DIR / \"VERSIONS\").relative_to(REPO_ROOT)}')\n\n    (VENDOR_DIR / 'source_commit.txt').write_text(commit + '\\n')\n    print(f'Wrote {(VENDOR_DIR / \"source_commit.txt\").relative_to(REPO_ROOT)}')\n\n    for file_path in COPY_FILES:\n        src_file = src_stdlib / file_path\n        if src_file.exists():\n            dest_file = STDLIB_DIR / file_path\n            dest_file.parent.mkdir(parents=True, exist_ok=True)\n            shutil.copy2(src_file, dest_file)\n        else:\n            raise ValueError(f'{file_path} not found in typeshed')\n    print(f'Copied {len(COPY_FILES)} stdlib typeshed files')\n\n    # copy pyi files from CUSTOM_DIR into STDLIB_DIR\n    custom_count = 0\n    for file in CUSTOM_DIR.glob('*.pyi'):\n        shutil.copy2(file, STDLIB_DIR)\n        custom_count += 1\n    print(f'Copied {custom_count} custom typeshed files')\n\n    return 0\n\n\nif __name__ == '__main__':\n    exit(main())\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/source_commit.txt",
    "content": "0e16ea31d2e188fdc126cb31e7c4fcc6b5a8da96\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/VERSIONS",
    "content": "# DO NOT EDIT THIS FILE DIRECTLY\n# instead edit crates/monty-typeshed/update.py\n# this file should match the modules\n# which monty's minimimal typeshed includes\n\n_collections_abc: 3.3-\n_typeshed: 3.0-  # not present at runtime, only for type checking\nasyncio: 3.4-\nbuiltins: 3.0-\ncollections: 3.0-\ndataclasses: 3.7-\nmath: 3.0-\nos: 3.0-\npathlib: 3.4-\npathlib.types: 3.14-\nre: 3.0-\nsys: 3.0-\ntyping: 3.5-\ntyping_extensions: 3.7-\ntypes: 3.0-\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/_collections_abc.pyi",
    "content": "import sys\nfrom abc import abstractmethod\nfrom types import MappingProxyType\nfrom typing import (  # noqa: Y022,Y038,UP035,Y057,RUF100\n    AbstractSet as Set,\n    AsyncGenerator as AsyncGenerator,\n    AsyncIterable as AsyncIterable,\n    AsyncIterator as AsyncIterator,\n    Awaitable as Awaitable,\n    ByteString as ByteString,\n    Callable as Callable,\n    ClassVar,\n    Collection as Collection,\n    Container as Container,\n    Coroutine as Coroutine,\n    Generator as Generator,\n    Generic,\n    Hashable as Hashable,\n    ItemsView as ItemsView,\n    Iterable as Iterable,\n    Iterator as Iterator,\n    KeysView as KeysView,\n    Mapping as Mapping,\n    MappingView as MappingView,\n    MutableMapping as MutableMapping,\n    MutableSequence as MutableSequence,\n    MutableSet as MutableSet,\n    Protocol,\n    Reversible as Reversible,\n    Sequence as Sequence,\n    Sized as Sized,\n    TypeVar,\n    ValuesView as ValuesView,\n    final,\n    runtime_checkable,\n)\n\n__all__ = [\n    'Awaitable',\n    'Coroutine',\n    'AsyncIterable',\n    'AsyncIterator',\n    'AsyncGenerator',\n    'Hashable',\n    'Iterable',\n    'Iterator',\n    'Generator',\n    'Reversible',\n    'Sized',\n    'Container',\n    'Callable',\n    'Collection',\n    'Set',\n    'MutableSet',\n    'Mapping',\n    'MutableMapping',\n    'MappingView',\n    'KeysView',\n    'ItemsView',\n    'ValuesView',\n    'Sequence',\n    'MutableSequence',\n    'ByteString',\n]\nif sys.version_info >= (3, 12):\n    __all__ += ['Buffer']\n\n_KT_co = TypeVar('_KT_co', covariant=True)  # Key type covariant containers.\n_VT_co = TypeVar('_VT_co', covariant=True)  # Value type covariant containers.\n\n@final\nclass dict_keys(KeysView[_KT_co], Generic[_KT_co, _VT_co]):  # undocumented\n    def __eq__(self, value: object, /) -> bool: ...\n    def __reversed__(self) -> Iterator[_KT_co]: ...\n    __hash__: ClassVar[None]  # type: ignore[assignment]\n    if sys.version_info >= (3, 13):\n        def isdisjoint(self, other: Iterable[_KT_co], /) -> bool: ...\n    if sys.version_info >= (3, 10):\n        @property\n        def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ...\n\n@final\nclass dict_values(ValuesView[_VT_co], Generic[_KT_co, _VT_co]):  # undocumented\n    def __reversed__(self) -> Iterator[_VT_co]: ...\n    if sys.version_info >= (3, 10):\n        @property\n        def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ...\n\n@final\nclass dict_items(ItemsView[_KT_co, _VT_co]):  # undocumented\n    def __eq__(self, value: object, /) -> bool: ...\n    def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...\n    __hash__: ClassVar[None]  # type: ignore[assignment]\n    if sys.version_info >= (3, 13):\n        def isdisjoint(self, other: Iterable[tuple[_KT_co, _VT_co]], /) -> bool: ...\n    if sys.version_info >= (3, 10):\n        @property\n        def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ...\n\nif sys.version_info >= (3, 12):\n    @runtime_checkable\n    class Buffer(Protocol):\n        __slots__ = ()\n        @abstractmethod\n        def __buffer__(self, flags: int, /) -> memoryview: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/_typeshed/__init__.pyi",
    "content": "# Utility types for typeshed\n#\n# See the README.md file in this directory for more information.\n\nimport sys\nfrom collections.abc import Awaitable, Callable, Iterable, Iterator, Sequence, Set as AbstractSet, Sized\nfrom dataclasses import Field\nfrom os import PathLike\nfrom types import FrameType, TracebackType\nfrom typing import (\n    Any,\n    AnyStr,\n    ClassVar,\n    Final,\n    Generic,\n    Literal,\n    Protocol,\n    SupportsFloat,\n    SupportsIndex,\n    SupportsInt,\n    TypeVar,\n    final,\n    overload,\n)\n\nfrom typing_extensions import Buffer, LiteralString, Self as _Self, TypeAlias\n\n_KT = TypeVar('_KT')\n_KT_co = TypeVar('_KT_co', covariant=True)\n_KT_contra = TypeVar('_KT_contra', contravariant=True)\n_VT = TypeVar('_VT')\n_VT_co = TypeVar('_VT_co', covariant=True)\n_T = TypeVar('_T')\n_T_co = TypeVar('_T_co', covariant=True)\n_T_contra = TypeVar('_T_contra', contravariant=True)\n\n# Alternative to `typing_extensions.Self`, exclusively for use with `__new__`\n# in metaclasses:\n#     def __new__(cls: type[Self], ...) -> Self: ...\n# In other cases, use `typing_extensions.Self`.\nSelf = TypeVar('Self')\n\n# covariant version of typing.AnyStr, useful for protocols\nAnyStr_co = TypeVar('AnyStr_co', str, bytes, covariant=True)\n\n# For partially known annotations. Usually, fields where type annotations\n# haven't been added are left unannotated, but in some situations this\n# isn't possible or a type is already partially known. In cases like these,\n# use Incomplete instead of Any as a marker. For example, use\n# \"Incomplete | None\" instead of \"Any | None\".\nIncomplete: TypeAlias = Any  # stable\n\n# To describe a function parameter that is unused and will work with anything.\nUnused: TypeAlias = object  # stable\n\n# Marker for return types that include None, but where forcing the user to\n# check for None can be detrimental. Sometimes called \"the Any trick\". See\n# https://typing.python.org/en/latest/guides/writing_stubs.html#the-any-trick\n# for more information.\nMaybeNone: TypeAlias = Any  # stable\n\n# Used to mark arguments that default to a sentinel value. This prevents\n# stubtest from complaining about the default value not matching.\n#\n# def foo(x: int | None = sentinel) -> None: ...\n#\n# In cases where the sentinel object is exported and can be used by user code,\n# a construct like this is better:\n#\n# _SentinelType = NewType(\"_SentinelType\", object)  # does not exist at runtime\n# sentinel: Final[_SentinelType]\n# def foo(x: int | None | _SentinelType = ...) -> None: ...\nsentinel: Any  # stable\n\n# stable\nclass IdentityFunction(Protocol):\n    def __call__(self, x: _T, /) -> _T: ...\n\n# stable\nclass SupportsNext(Protocol[_T_co]):\n    def __next__(self) -> _T_co: ...\n\n# stable\nclass SupportsAnext(Protocol[_T_co]):\n    def __anext__(self) -> Awaitable[_T_co]: ...\n\nclass SupportsBool(Protocol):\n    def __bool__(self) -> bool: ...\n\n# Comparison protocols\nclass SupportsDunderLT(Protocol[_T_contra]):\n    def __lt__(self, other: _T_contra, /) -> SupportsBool: ...\n\nclass SupportsDunderGT(Protocol[_T_contra]):\n    def __gt__(self, other: _T_contra, /) -> SupportsBool: ...\n\nclass SupportsDunderLE(Protocol[_T_contra]):\n    def __le__(self, other: _T_contra, /) -> SupportsBool: ...\n\nclass SupportsDunderGE(Protocol[_T_contra]):\n    def __ge__(self, other: _T_contra, /) -> SupportsBool: ...\n\nclass SupportsAllComparisons(\n    SupportsDunderLT[Any], SupportsDunderGT[Any], SupportsDunderLE[Any], SupportsDunderGE[Any], Protocol\n): ...\n\nSupportsRichComparison: TypeAlias = SupportsDunderLT[Any] | SupportsDunderGT[Any]\nSupportsRichComparisonT = TypeVar('SupportsRichComparisonT', bound=SupportsRichComparison)\n\n# Dunder protocols\n\nclass SupportsAdd(Protocol[_T_contra, _T_co]):\n    def __add__(self, x: _T_contra, /) -> _T_co: ...\n\nclass SupportsRAdd(Protocol[_T_contra, _T_co]):\n    def __radd__(self, x: _T_contra, /) -> _T_co: ...\n\nclass SupportsSub(Protocol[_T_contra, _T_co]):\n    def __sub__(self, x: _T_contra, /) -> _T_co: ...\n\nclass SupportsRSub(Protocol[_T_contra, _T_co]):\n    def __rsub__(self, x: _T_contra, /) -> _T_co: ...\n\nclass SupportsMul(Protocol[_T_contra, _T_co]):\n    def __mul__(self, x: _T_contra, /) -> _T_co: ...\n\nclass SupportsRMul(Protocol[_T_contra, _T_co]):\n    def __rmul__(self, x: _T_contra, /) -> _T_co: ...\n\nclass SupportsDivMod(Protocol[_T_contra, _T_co]):\n    def __divmod__(self, other: _T_contra, /) -> _T_co: ...\n\nclass SupportsRDivMod(Protocol[_T_contra, _T_co]):\n    def __rdivmod__(self, other: _T_contra, /) -> _T_co: ...\n\n# This protocol is generic over the iterator type, while Iterable is\n# generic over the type that is iterated over.\nclass SupportsIter(Protocol[_T_co]):\n    def __iter__(self) -> _T_co: ...\n\n# This protocol is generic over the iterator type, while AsyncIterable is\n# generic over the type that is iterated over.\nclass SupportsAiter(Protocol[_T_co]):\n    def __aiter__(self) -> _T_co: ...\n\nclass SupportsLen(Protocol):\n    def __len__(self) -> int: ...\n\nclass SupportsLenAndGetItem(Protocol[_T_co]):\n    def __len__(self) -> int: ...\n    def __getitem__(self, k: int, /) -> _T_co: ...\n\nclass SupportsTrunc(Protocol):\n    def __trunc__(self) -> int: ...\n\n# Mapping-like protocols\n\n# stable\nclass SupportsItems(Protocol[_KT_co, _VT_co]):\n    def items(self) -> AbstractSet[tuple[_KT_co, _VT_co]]: ...\n\n# stable\nclass SupportsKeysAndGetItem(Protocol[_KT, _VT_co]):\n    def keys(self) -> Iterable[_KT]: ...\n    def __getitem__(self, key: _KT, /) -> _VT_co: ...\n\n# stable\nclass SupportsGetItem(Protocol[_KT_contra, _VT_co]):\n    def __getitem__(self, key: _KT_contra, /) -> _VT_co: ...\n\n# stable\nclass SupportsContainsAndGetItem(Protocol[_KT_contra, _VT_co]):\n    def __contains__(self, x: Any, /) -> bool: ...\n    def __getitem__(self, key: _KT_contra, /) -> _VT_co: ...\n\n# stable\nclass SupportsItemAccess(Protocol[_KT_contra, _VT]):\n    def __contains__(self, x: Any, /) -> bool: ...\n    def __getitem__(self, key: _KT_contra, /) -> _VT: ...\n    def __setitem__(self, key: _KT_contra, value: _VT, /) -> None: ...\n    def __delitem__(self, key: _KT_contra, /) -> None: ...\n\nStrPath: TypeAlias = str | PathLike[str]  # stable\nBytesPath: TypeAlias = bytes | PathLike[bytes]  # stable\nGenericPath: TypeAlias = AnyStr | PathLike[AnyStr]\nStrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]  # stable\n\nOpenTextModeUpdating: TypeAlias = Literal[\n    'r+',\n    '+r',\n    'rt+',\n    'r+t',\n    '+rt',\n    'tr+',\n    't+r',\n    '+tr',\n    'w+',\n    '+w',\n    'wt+',\n    'w+t',\n    '+wt',\n    'tw+',\n    't+w',\n    '+tw',\n    'a+',\n    '+a',\n    'at+',\n    'a+t',\n    '+at',\n    'ta+',\n    't+a',\n    '+ta',\n    'x+',\n    '+x',\n    'xt+',\n    'x+t',\n    '+xt',\n    'tx+',\n    't+x',\n    '+tx',\n]\nOpenTextModeWriting: TypeAlias = Literal['w', 'wt', 'tw', 'a', 'at', 'ta', 'x', 'xt', 'tx']\nOpenTextModeReading: TypeAlias = Literal['r', 'rt', 'tr', 'U', 'rU', 'Ur', 'rtU', 'rUt', 'Urt', 'trU', 'tUr', 'Utr']\nOpenTextMode: TypeAlias = OpenTextModeUpdating | OpenTextModeWriting | OpenTextModeReading\nOpenBinaryModeUpdating: TypeAlias = Literal[\n    'rb+',\n    'r+b',\n    '+rb',\n    'br+',\n    'b+r',\n    '+br',\n    'wb+',\n    'w+b',\n    '+wb',\n    'bw+',\n    'b+w',\n    '+bw',\n    'ab+',\n    'a+b',\n    '+ab',\n    'ba+',\n    'b+a',\n    '+ba',\n    'xb+',\n    'x+b',\n    '+xb',\n    'bx+',\n    'b+x',\n    '+bx',\n]\nOpenBinaryModeWriting: TypeAlias = Literal['wb', 'bw', 'ab', 'ba', 'xb', 'bx']\nOpenBinaryModeReading: TypeAlias = Literal['rb', 'br', 'rbU', 'rUb', 'Urb', 'brU', 'bUr', 'Ubr']\nOpenBinaryMode: TypeAlias = OpenBinaryModeUpdating | OpenBinaryModeReading | OpenBinaryModeWriting\n\n# stable\nclass HasFileno(Protocol):\n    def fileno(self) -> int: ...\n\nFileDescriptor: TypeAlias = int  # stable\nFileDescriptorLike: TypeAlias = int | HasFileno  # stable\nFileDescriptorOrPath: TypeAlias = int | StrOrBytesPath\n\n# stable\nclass SupportsRead(Protocol[_T_co]):\n    def read(self, length: int = ..., /) -> _T_co: ...\n\n# stable\nclass SupportsReadline(Protocol[_T_co]):\n    def readline(self, length: int = ..., /) -> _T_co: ...\n\n# stable\nclass SupportsNoArgReadline(Protocol[_T_co]):\n    def readline(self) -> _T_co: ...\n\n# stable\nclass SupportsWrite(Protocol[_T_contra]):\n    def write(self, s: _T_contra, /) -> object: ...\n\n# stable\nclass SupportsFlush(Protocol):\n    def flush(self) -> object: ...\n\n# Suitable for dictionary view objects\nclass Viewable(Protocol[_T_co]):\n    def __len__(self) -> int: ...\n    def __iter__(self) -> Iterator[_T_co]: ...\n\nclass SupportsGetItemViewable(Protocol[_KT, _VT_co]):\n    def __len__(self) -> int: ...\n    def __iter__(self) -> Iterator[_KT]: ...\n    def __getitem__(self, key: _KT, /) -> _VT_co: ...\n\n# Unfortunately PEP 688 does not allow us to distinguish read-only\n# from writable buffers. We use these aliases for readability for now.\n# Perhaps a future extension of the buffer protocol will allow us to\n# distinguish these cases in the type system.\nReadOnlyBuffer: TypeAlias = Buffer  # stable\n# Anything that implements the read-write buffer interface.\nWriteableBuffer: TypeAlias = Buffer\n# Same as WriteableBuffer, but also includes read-only buffer types (like bytes).\nReadableBuffer: TypeAlias = Buffer  # stable\n\nclass SliceableBuffer(Buffer, Protocol):\n    def __getitem__(self, slice: slice[SupportsIndex | None], /) -> Sequence[int]: ...\n\nclass IndexableBuffer(Buffer, Protocol):\n    def __getitem__(self, i: int, /) -> int: ...\n\nclass SupportsGetItemBuffer(SliceableBuffer, IndexableBuffer, Protocol):\n    def __contains__(self, x: Any, /) -> bool: ...\n    @overload\n    def __getitem__(self, slice: slice[SupportsIndex | None], /) -> Sequence[int]: ...\n    @overload\n    def __getitem__(self, i: int, /) -> int: ...\n\nclass SizedBuffer(Sized, Buffer, Protocol): ...\n\nExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType]\nOptExcInfo: TypeAlias = ExcInfo | tuple[None, None, None]\n\n# stable\nif sys.version_info >= (3, 10):\n    from types import NoneType as NoneType\nelse:\n    # Used by type checkers for checks involving None (does not exist at runtime)\n    @final\n    class NoneType:\n        def __bool__(self) -> Literal[False]: ...\n\n# This is an internal CPython type that is like, but subtly different from, a NamedTuple\n# Subclasses of this type are found in multiple modules.\n# In typeshed, `structseq` is only ever used as a mixin in combination with a fixed-length `Tuple`\n# See discussion at #6546 & #6560\n# `structseq` classes are unsubclassable, so are all decorated with `@final`.\nclass structseq(Generic[_T_co]):\n    n_fields: Final[int]\n    n_unnamed_fields: Final[int]\n    n_sequence_fields: Final[int]\n    # The first parameter will generally only take an iterable of a specific length.\n    # E.g. `os.uname_result` takes any iterable of length exactly 5.\n    #\n    # The second parameter will accept a dict of any kind without raising an exception,\n    # but only has any meaning if you supply it a dict where the keys are strings.\n    # https://github.com/python/typeshed/pull/6560#discussion_r767149830\n    def __new__(cls, sequence: Iterable[_T_co], dict: dict[str, Any] = ...) -> _Self: ...\n    if sys.version_info >= (3, 13):\n        def __replace__(self, **kwargs: Any) -> _Self: ...\n\n# Superset of typing.AnyStr that also includes LiteralString\nAnyOrLiteralStr = TypeVar('AnyOrLiteralStr', str, bytes, LiteralString)\n\n# Represents when str or LiteralStr is acceptable. Useful for string processing\n# APIs where literalness of return value depends on literalness of inputs\nStrOrLiteralStr = TypeVar('StrOrLiteralStr', LiteralString, str)\n\n# Objects suitable to be passed to sys.setprofile, threading.setprofile, and similar\nProfileFunction: TypeAlias = Callable[[FrameType, str, Any], object]\n\n# Objects suitable to be passed to sys.settrace, threading.settrace, and similar\nTraceFunction: TypeAlias = Callable[[FrameType, str, Any], TraceFunction | None]\n\n# experimental\n# Might not work as expected for pyright, see\n#   https://github.com/python/typeshed/pull/9362\n#   https://github.com/microsoft/pyright/issues/4339\nclass DataclassInstance(Protocol):\n    __dataclass_fields__: ClassVar[dict[str, Field[Any]]]\n\n# Anything that can be passed to the int/float constructors\nif sys.version_info >= (3, 14):\n    ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex\nelse:\n    ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc\nConvertibleToFloat: TypeAlias = str | ReadableBuffer | SupportsFloat | SupportsIndex\n\n# A few classes updated from Foo(str, Enum) to Foo(StrEnum). This is a convenience so these\n# can be accurate on all python versions without getting too wordy\nif sys.version_info >= (3, 11):\n    from enum import StrEnum as StrEnum\nelse:\n    from enum import Enum\n\n    class StrEnum(str, Enum): ...\n\n# Objects that appear in annotations or in type expressions.\n# Similar to PEP 747's TypeForm but a little broader.\nAnnotationForm: TypeAlias = Any\n\nif sys.version_info >= (3, 14):\n    from annotationlib import Format\n\n    # These return annotations, which can be arbitrary objects\n    AnnotateFunc: TypeAlias = Callable[[Format], dict[str, AnnotationForm]]\n    EvaluateFunc: TypeAlias = Callable[[Format], AnnotationForm]\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/asyncio.pyi",
    "content": "from collections.abc import Awaitable, Generator\nfrom typing import Any, Literal, TypeAlias, TypeVar, overload\n\n_T = TypeVar('_T')\n_T1 = TypeVar('_T1')\n_T2 = TypeVar('_T2')\n_T3 = TypeVar('_T3')\n_T4 = TypeVar('_T4')\n_T5 = TypeVar('_T5')\n_T6 = TypeVar('_T6')\n\nclass _Future(Awaitable[_T]):\n    \"\"\"\n    Minimal copy of Future from _typeshed/stdlib/_asyncio.pyi\n    \"\"\"\n    def __iter__(self) -> Generator[Any, None, _T]: ...\n    def __await__(self) -> Generator[Any, None, _T]: ...\n\n_FutureLike: TypeAlias = _Future[_T] | Awaitable[_T]\n\ndef run(main: Awaitable[_T], *, debug: bool | None = None, loop_factory: Any = None) -> _T: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: Literal[False] = False\n) -> _Future[tuple[_T1]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3, _T4]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3, _T4, _T5]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    coro_or_future6: _FutureLike[_T6],\n    /,\n    *,\n    return_exceptions: Literal[False] = False,\n) -> _Future[tuple[_T1, _T2, _T3, _T4, _T5, _T6]]: ...\n@overload\ndef gather(*coros_or_futures: _FutureLike[_T], return_exceptions: Literal[False] = False) -> _Future[list[_T]]: ...\n@overload\ndef gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: bool) -> _Future[tuple[_T1 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: bool\n) -> _Future[tuple[_T1 | BaseException, _T2 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException]]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[\n    tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException, _T5 | BaseException]\n]: ...\n@overload\ndef gather(\n    coro_or_future1: _FutureLike[_T1],\n    coro_or_future2: _FutureLike[_T2],\n    coro_or_future3: _FutureLike[_T3],\n    coro_or_future4: _FutureLike[_T4],\n    coro_or_future5: _FutureLike[_T5],\n    coro_or_future6: _FutureLike[_T6],\n    /,\n    *,\n    return_exceptions: bool,\n) -> _Future[\n    tuple[\n        _T1 | BaseException,\n        _T2 | BaseException,\n        _T3 | BaseException,\n        _T4 | BaseException,\n        _T5 | BaseException,\n        _T6 | BaseException,\n    ]\n]: ...\n@overload\ndef gather(*coros_or_futures: _FutureLike[_T], return_exceptions: bool) -> _Future[list[_T | BaseException]]: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/builtins.pyi",
    "content": "import _sitebuiltins\nimport sys\nimport types\nfrom _collections_abc import dict_items, dict_keys, dict_values\nfrom collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized\nfrom types import GenericAlias, TracebackType\nfrom typing import (\n    Any,\n    ClassVar,\n    Final,\n    Generic,\n    MutableMapping,\n    MutableSequence,\n    Protocol,\n    Sequence,\n    SupportsAbs,\n    SupportsBytes,\n    SupportsFloat,\n    SupportsIndex,\n    TypeVar,\n    final,\n    overload,\n    type_check_only,\n)\n\nimport _typeshed\nfrom _typeshed import (\n    AnnotationForm,\n    ConvertibleToFloat,\n    ConvertibleToInt,\n    ReadableBuffer,\n    SupportsAdd,\n    SupportsAnext,\n    SupportsDivMod,\n    SupportsFlush,\n    SupportsKeysAndGetItem,\n    SupportsLenAndGetItem,\n    SupportsNext,\n    SupportsRAdd,\n    SupportsRDivMod,\n    SupportsRichComparison,\n    SupportsRichComparisonT,\n    SupportsWrite,\n)\nfrom typing_extensions import (\n    Literal,\n    LiteralString,\n    ParamSpec,\n    Self,\n    TypeAlias,\n    TypeVarTuple,\n    deprecated,\n    disjoint_base,\n)\n\nif sys.version_info >= (3, 14):\n    from _typeshed import AnnotateFunc\n_T = TypeVar('_T')\n_I = TypeVar('_I', default=int)\n_T_co = TypeVar('_T_co', covariant=True)\n_T_contra = TypeVar('_T_contra', contravariant=True)\n_R_co = TypeVar('_R_co', covariant=True)\n_KT = TypeVar('_KT')\n_VT = TypeVar('_VT')\n_S = TypeVar('_S')\n_T1 = TypeVar('_T1')\n_T2 = TypeVar('_T2')\n_T3 = TypeVar('_T3')\n_T4 = TypeVar('_T4')\n_T5 = TypeVar('_T5')\n_SupportsNextT_co = TypeVar('_SupportsNextT_co', bound=SupportsNext[Any], covariant=True)\n_SupportsAnextT_co = TypeVar('_SupportsAnextT_co', bound=SupportsAnext[Any], covariant=True)\n_AwaitableT = TypeVar('_AwaitableT', bound=Awaitable[Any])\n_AwaitableT_co = TypeVar('_AwaitableT_co', bound=Awaitable[Any], covariant=True)\n_P = ParamSpec('_P')\n_StartT_co = TypeVar('_StartT_co', covariant=True, default=Any)\n_StopT_co = TypeVar('_StopT_co', covariant=True, default=_StartT_co)\n_StepT_co = TypeVar('_StepT_co', covariant=True, default=_StartT_co | _StopT_co)\n\n@disjoint_base\nclass object:\n    __doc__: str | None\n    __dict__: dict[str, Any]\n    __module__: str\n    __annotations__: dict[str, Any]\n\n    @property\n    def __class__(self) -> type[Self]: ...\n    @__class__.setter\n    def __class__(self, type: type[Self], /) -> None: ...\n    def __init__(self) -> None: ...\n    def __new__(cls) -> Self: ...\n    def __setattr__(self, name: str, value: Any, /) -> None: ...\n    def __delattr__(self, name: str, /) -> None: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __ne__(self, value: object, /) -> bool: ...\n    def __str__(self) -> str: ...\n    def __repr__(self) -> str: ...\n    def __hash__(self) -> int: ...\n    def __format__(self, format_spec: str, /) -> str: ...\n    def __getattribute__(self, name: str, /) -> Any: ...\n    def __sizeof__(self) -> int: ...\n    def __reduce__(self) -> str | tuple[Any, ...]: ...\n    def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]: ...\n    if sys.version_info >= (3, 11):\n        def __getstate__(self) -> object: ...\n\n    def __dir__(self) -> Iterable[str]: ...\n    def __init_subclass__(cls) -> None: ...\n    @classmethod\n    def __subclasshook__(cls, subclass: type, /) -> bool: ...\n\n@disjoint_base\nclass type:\n    @property\n    def __base__(self) -> type | None: ...\n    __bases__: tuple[type, ...]\n\n    @property\n    def __basicsize__(self) -> int: ...\n    __dict__: Final[types.MappingProxyType[str, Any]]\n\n    @property\n    def __dictoffset__(self) -> int: ...\n    @property\n    def __flags__(self) -> int: ...\n    @property\n    def __itemsize__(self) -> int: ...\n    __module__: str\n\n    @property\n    def __mro__(self) -> tuple[type, ...]: ...\n    __name__: str\n    __qualname__: str\n\n    @property\n    def __text_signature__(self) -> str | None: ...\n    @property\n    def __weakrefoffset__(self) -> int: ...\n    @overload\n    def __init__(self, o: object, /) -> None: ...\n    @overload\n    def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ...\n    @overload\n    def __new__(cls, o: object, /) -> type: ...\n    @overload\n    def __new__(\n        cls: type[_typeshed.Self], name: str, bases: tuple[type, ...], namespace: dict[str, Any], /, **kwds: Any\n    ) -> _typeshed.Self: ...\n    def __call__(self, *args: Any, **kwds: Any) -> Any: ...\n    def __subclasses__(self: _typeshed.Self) -> list[_typeshed.Self]: ...\n    def mro(self) -> list[type]: ...\n    def __instancecheck__(self, instance: Any, /) -> bool: ...\n    def __subclasscheck__(self, subclass: type, /) -> bool: ...\n    @classmethod\n    def __prepare__(metacls, name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]: ...\n    if sys.version_info >= (3, 10):\n        def __or__(self: _typeshed.Self, value: Any, /) -> types.UnionType | _typeshed.Self: ...\n        def __ror__(self: _typeshed.Self, value: Any, /) -> types.UnionType | _typeshed.Self: ...\n    if sys.version_info >= (3, 12):\n        __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]\n    __annotations__: dict[str, AnnotationForm]\n    if sys.version_info >= (3, 14):\n        __annotate__: AnnotateFunc | None\n\n_PositiveInteger: TypeAlias = Literal[\n    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25\n]\n_NegativeInteger: TypeAlias = Literal[\n    -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20\n]\n_LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0]\n\n@disjoint_base\nclass int:\n    @overload\n    def __new__(cls, x: ConvertibleToInt = 0, /) -> Self: ...\n    @overload\n    def __new__(cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self: ...\n    def as_integer_ratio(self) -> tuple[int, Literal[1]]: ...\n    @property\n    def real(self) -> int: ...\n    @property\n    def imag(self) -> Literal[0]: ...\n    @property\n    def numerator(self) -> int: ...\n    @property\n    def denominator(self) -> Literal[1]: ...\n    def conjugate(self) -> int: ...\n    def bit_length(self) -> int: ...\n    if sys.version_info >= (3, 10):\n        def bit_count(self) -> int: ...\n    if sys.version_info >= (3, 11):\n        def to_bytes(\n            self, length: SupportsIndex = 1, byteorder: Literal['little', 'big'] = 'big', *, signed: bool = False\n        ) -> bytes: ...\n        @classmethod\n        def from_bytes(\n            cls,\n            bytes: Iterable[SupportsIndex] | SupportsBytes | ReadableBuffer,\n            byteorder: Literal['little', 'big'] = 'big',\n            *,\n            signed: bool = False,\n        ) -> Self: ...\n    else:\n        def to_bytes(\n            self, length: SupportsIndex, byteorder: Literal['little', 'big'], *, signed: bool = False\n        ) -> bytes: ...\n        @classmethod\n        def from_bytes(\n            cls,\n            bytes: Iterable[SupportsIndex] | SupportsBytes | ReadableBuffer,\n            byteorder: Literal['little', 'big'],\n            *,\n            signed: bool = False,\n        ) -> Self: ...\n    if sys.version_info >= (3, 12):\n        def is_integer(self) -> Literal[True]: ...\n\n    def __add__(self, value: int, /) -> int: ...\n    def __sub__(self, value: int, /) -> int: ...\n    def __mul__(self, value: int, /) -> int: ...\n    def __floordiv__(self, value: int, /) -> int: ...\n    def __truediv__(self, value: int, /) -> float: ...\n    def __mod__(self, value: int, /) -> int: ...\n    def __divmod__(self, value: int, /) -> tuple[int, int]: ...\n    def __radd__(self, value: int, /) -> int: ...\n    def __rsub__(self, value: int, /) -> int: ...\n    def __rmul__(self, value: int, /) -> int: ...\n    def __rfloordiv__(self, value: int, /) -> int: ...\n    def __rtruediv__(self, value: int, /) -> float: ...\n    def __rmod__(self, value: int, /) -> int: ...\n    def __rdivmod__(self, value: int, /) -> tuple[int, int]: ...\n    @overload\n    def __pow__(self, x: Literal[0], /) -> Literal[1]: ...\n    @overload\n    def __pow__(self, value: Literal[0], mod: None, /) -> Literal[1]: ...\n    @overload\n    def __pow__(self, value: _PositiveInteger, mod: None = None, /) -> int: ...\n    @overload\n    def __pow__(self, value: _NegativeInteger, mod: None = None, /) -> float: ...\n    @overload\n    def __pow__(self, value: int, mod: None = None, /) -> Any: ...\n    @overload\n    def __pow__(self, value: int, mod: int, /) -> int: ...\n    def __rpow__(self, value: int, mod: int | None = None, /) -> Any: ...\n    def __and__(self, value: int, /) -> int: ...\n    def __or__(self, value: int, /) -> int: ...\n    def __xor__(self, value: int, /) -> int: ...\n    def __lshift__(self, value: int, /) -> int: ...\n    def __rshift__(self, value: int, /) -> int: ...\n    def __rand__(self, value: int, /) -> int: ...\n    def __ror__(self, value: int, /) -> int: ...\n    def __rxor__(self, value: int, /) -> int: ...\n    def __rlshift__(self, value: int, /) -> int: ...\n    def __rrshift__(self, value: int, /) -> int: ...\n    def __neg__(self) -> int: ...\n    def __pos__(self) -> int: ...\n    def __invert__(self) -> int: ...\n    def __trunc__(self) -> int: ...\n    def __ceil__(self) -> int: ...\n    def __floor__(self) -> int: ...\n    if sys.version_info >= (3, 14):\n        def __round__(self, ndigits: SupportsIndex | None = None, /) -> int: ...\n    else:\n        def __round__(self, ndigits: SupportsIndex = ..., /) -> int: ...\n\n    def __getnewargs__(self) -> tuple[int]: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __ne__(self, value: object, /) -> bool: ...\n    def __lt__(self, value: int, /) -> bool: ...\n    def __le__(self, value: int, /) -> bool: ...\n    def __gt__(self, value: int, /) -> bool: ...\n    def __ge__(self, value: int, /) -> bool: ...\n    def __float__(self) -> float: ...\n    def __int__(self) -> int: ...\n    def __abs__(self) -> int: ...\n    def __hash__(self) -> int: ...\n    def __bool__(self) -> bool: ...\n    def __index__(self) -> int: ...\n    def __format__(self, format_spec: str, /) -> str: ...\n\n@disjoint_base\nclass float:\n    def __new__(cls, x: ConvertibleToFloat = 0, /) -> Self: ...\n    def as_integer_ratio(self) -> tuple[int, int]: ...\n    def hex(self) -> str: ...\n    def is_integer(self) -> bool: ...\n    @classmethod\n    def fromhex(cls, string: str, /) -> Self: ...\n    @property\n    def real(self) -> float: ...\n    @property\n    def imag(self) -> float: ...\n    def conjugate(self) -> float: ...\n    def __add__(self, value: float, /) -> float: ...\n    def __sub__(self, value: float, /) -> float: ...\n    def __mul__(self, value: float, /) -> float: ...\n    def __floordiv__(self, value: float, /) -> float: ...\n    def __truediv__(self, value: float, /) -> float: ...\n    def __mod__(self, value: float, /) -> float: ...\n    def __divmod__(self, value: float, /) -> tuple[float, float]: ...\n    @overload\n    def __pow__(self, value: int, mod: None = None, /) -> float: ...\n    @overload\n    def __pow__(self, value: float, mod: None = None, /) -> Any: ...\n    def __radd__(self, value: float, /) -> float: ...\n    def __rsub__(self, value: float, /) -> float: ...\n    def __rmul__(self, value: float, /) -> float: ...\n    def __rfloordiv__(self, value: float, /) -> float: ...\n    def __rtruediv__(self, value: float, /) -> float: ...\n    def __rmod__(self, value: float, /) -> float: ...\n    def __rdivmod__(self, value: float, /) -> tuple[float, float]: ...\n    @overload\n    def __rpow__(self, value: _PositiveInteger, mod: None = None, /) -> float: ...\n    @overload\n    def __rpow__(self, value: _NegativeInteger, mod: None = None, /) -> complex: ...\n    @overload\n    def __rpow__(self, value: float, mod: None = None, /) -> Any: ...\n    def __getnewargs__(self) -> tuple[float]: ...\n    def __trunc__(self) -> int: ...\n    def __ceil__(self) -> int: ...\n    def __floor__(self) -> int: ...\n    @overload\n    def __round__(self, ndigits: None = None, /) -> int: ...\n    @overload\n    def __round__(self, ndigits: SupportsIndex, /) -> float: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __ne__(self, value: object, /) -> bool: ...\n    def __lt__(self, value: float, /) -> bool: ...\n    def __le__(self, value: float, /) -> bool: ...\n    def __gt__(self, value: float, /) -> bool: ...\n    def __ge__(self, value: float, /) -> bool: ...\n    def __neg__(self) -> float: ...\n    def __pos__(self) -> float: ...\n    def __int__(self) -> int: ...\n    def __float__(self) -> float: ...\n    def __abs__(self) -> float: ...\n    def __hash__(self) -> int: ...\n    def __bool__(self) -> bool: ...\n    def __format__(self, format_spec: str, /) -> str: ...\n    if sys.version_info >= (3, 14):\n        @classmethod\n        def from_number(cls, number: float | SupportsIndex | SupportsFloat, /) -> Self: ...\n\n@type_check_only\nclass _FormatMapMapping(Protocol):\n    def __getitem__(self, key: str, /) -> Any: ...\n\n@type_check_only\nclass _TranslateTable(Protocol):\n    def __getitem__(self, key: int, /) -> str | int | None: ...\n\n@disjoint_base\nclass str(Sequence[str]):\n    @overload\n    def __new__(cls, object: object = '') -> Self: ...\n    @overload\n    def __new__(cls, object: ReadableBuffer, encoding: str = 'utf-8', errors: str = 'strict') -> Self: ...\n    @overload\n    def capitalize(self: LiteralString) -> LiteralString: ...\n    @overload\n    def capitalize(self) -> str: ...\n    @overload\n    def casefold(self: LiteralString) -> LiteralString: ...\n    @overload\n    def casefold(self) -> str: ...\n    @overload\n    def center(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = ' ', /) -> LiteralString: ...\n    @overload\n    def center(self, width: SupportsIndex, fillchar: str = ' ', /) -> str: ...\n    def count(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ...\n    def encode(self, encoding: str = 'utf-8', errors: str = 'strict') -> bytes: ...\n    def endswith(\n        self, suffix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /\n    ) -> bool: ...\n    @overload\n    def expandtabs(self: LiteralString, tabsize: SupportsIndex = 8) -> LiteralString: ...\n    @overload\n    def expandtabs(self, tabsize: SupportsIndex = 8) -> str: ...\n    def find(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ...\n    @overload\n    def format(self: LiteralString, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ...\n    @overload\n    def format(self, *args: object, **kwargs: object) -> str: ...\n    def format_map(self, mapping: _FormatMapMapping, /) -> str: ...\n    def index(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ...\n    def isalnum(self) -> bool: ...\n    def isalpha(self) -> bool: ...\n    def isascii(self) -> bool: ...\n    def isdecimal(self) -> bool: ...\n    def isdigit(self) -> bool: ...\n    def isidentifier(self) -> bool: ...\n    def islower(self) -> bool: ...\n    def isnumeric(self) -> bool: ...\n    def isprintable(self) -> bool: ...\n    def isspace(self) -> bool: ...\n    def istitle(self) -> bool: ...\n    def isupper(self) -> bool: ...\n    @overload\n    def join(self: LiteralString, iterable: Iterable[LiteralString], /) -> LiteralString: ...\n    @overload\n    def join(self, iterable: Iterable[str], /) -> str: ...\n    @overload\n    def ljust(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = ' ', /) -> LiteralString: ...\n    @overload\n    def ljust(self, width: SupportsIndex, fillchar: str = ' ', /) -> str: ...\n    @overload\n    def lower(self: LiteralString) -> LiteralString: ...\n    @overload\n    def lower(self) -> str: ...\n    @overload\n    def lstrip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: ...\n    @overload\n    def lstrip(self, chars: str | None = None, /) -> str: ...\n    @overload\n    def partition(self: LiteralString, sep: LiteralString, /) -> tuple[LiteralString, LiteralString, LiteralString]: ...\n    @overload\n    def partition(self, sep: str, /) -> tuple[str, str, str]: ...\n    if sys.version_info >= (3, 13):\n        @overload\n        def replace(\n            self: LiteralString, old: LiteralString, new: LiteralString, /, count: SupportsIndex = -1\n        ) -> LiteralString: ...\n        @overload\n        def replace(self, old: str, new: str, /, count: SupportsIndex = -1) -> str: ...\n    else:\n        @overload\n        def replace(\n            self: LiteralString, old: LiteralString, new: LiteralString, count: SupportsIndex = -1, /\n        ) -> LiteralString: ...\n        @overload\n        def replace(self, old: str, new: str, count: SupportsIndex = -1, /) -> str: ...\n\n    @overload\n    def removeprefix(self: LiteralString, prefix: LiteralString, /) -> LiteralString: ...\n    @overload\n    def removeprefix(self, prefix: str, /) -> str: ...\n    @overload\n    def removesuffix(self: LiteralString, suffix: LiteralString, /) -> LiteralString: ...\n    @overload\n    def removesuffix(self, suffix: str, /) -> str: ...\n    def rfind(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ...\n    def rindex(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ...\n    @overload\n    def rjust(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = ' ', /) -> LiteralString: ...\n    @overload\n    def rjust(self, width: SupportsIndex, fillchar: str = ' ', /) -> str: ...\n    @overload\n    def rpartition(\n        self: LiteralString, sep: LiteralString, /\n    ) -> tuple[LiteralString, LiteralString, LiteralString]: ...\n    @overload\n    def rpartition(self, sep: str, /) -> tuple[str, str, str]: ...\n    @overload\n    def rsplit(\n        self: LiteralString, sep: LiteralString | None = None, maxsplit: SupportsIndex = -1\n    ) -> list[LiteralString]: ...\n    @overload\n    def rsplit(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: ...\n    @overload\n    def rstrip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: ...\n    @overload\n    def rstrip(self, chars: str | None = None, /) -> str: ...\n    @overload\n    def split(\n        self: LiteralString, sep: LiteralString | None = None, maxsplit: SupportsIndex = -1\n    ) -> list[LiteralString]: ...\n    @overload\n    def split(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: ...\n    @overload\n    def splitlines(self: LiteralString, keepends: bool = False) -> list[LiteralString]: ...\n    @overload\n    def splitlines(self, keepends: bool = False) -> list[str]: ...\n    def startswith(\n        self, prefix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /\n    ) -> bool: ...\n    @overload\n    def strip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: ...\n    @overload\n    def strip(self, chars: str | None = None, /) -> str: ...\n    @overload\n    def swapcase(self: LiteralString) -> LiteralString: ...\n    @overload\n    def swapcase(self) -> str: ...\n    @overload\n    def title(self: LiteralString) -> LiteralString: ...\n    @overload\n    def title(self) -> str: ...\n    def translate(self, table: _TranslateTable, /) -> str: ...\n    @overload\n    def upper(self: LiteralString) -> LiteralString: ...\n    @overload\n    def upper(self) -> str: ...\n    @overload\n    def zfill(self: LiteralString, width: SupportsIndex, /) -> LiteralString: ...\n    @overload\n    def zfill(self, width: SupportsIndex, /) -> str: ...\n    @staticmethod\n    @overload\n    def maketrans(x: dict[int, _T] | dict[str, _T] | dict[str | int, _T], /) -> dict[int, _T]: ...\n    @staticmethod\n    @overload\n    def maketrans(x: str, y: str, /) -> dict[int, int]: ...\n    @staticmethod\n    @overload\n    def maketrans(x: str, y: str, z: str, /) -> dict[int, int | None]: ...\n    @overload\n    def __add__(self: LiteralString, value: LiteralString, /) -> LiteralString: ...\n    @overload\n    def __add__(self, value: str, /) -> str: ...\n    def __contains__(self, key: str, /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __ge__(self, value: str, /) -> bool: ...\n    @overload\n    def __getitem__(self: LiteralString, key: SupportsIndex | slice[SupportsIndex | None], /) -> LiteralString: ...\n    @overload\n    def __getitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> str: ...\n    def __gt__(self, value: str, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    @overload\n    def __iter__(self: LiteralString) -> Iterator[LiteralString]: ...\n    @overload\n    def __iter__(self) -> Iterator[str]: ...\n    def __le__(self, value: str, /) -> bool: ...\n    def __len__(self) -> int: ...\n    def __lt__(self, value: str, /) -> bool: ...\n    @overload\n    def __mod__(self: LiteralString, value: LiteralString | tuple[LiteralString, ...], /) -> LiteralString: ...\n    @overload\n    def __mod__(self, value: Any, /) -> str: ...\n    @overload\n    def __mul__(self: LiteralString, value: SupportsIndex, /) -> LiteralString: ...\n    @overload\n    def __mul__(self, value: SupportsIndex, /) -> str: ...\n    def __ne__(self, value: object, /) -> bool: ...\n    @overload\n    def __rmul__(self: LiteralString, value: SupportsIndex, /) -> LiteralString: ...\n    @overload\n    def __rmul__(self, value: SupportsIndex, /) -> str: ...\n    def __getnewargs__(self) -> tuple[str]: ...\n    def __format__(self, format_spec: str, /) -> str: ...\n\n@disjoint_base\nclass bytes(Sequence[int]):\n    @overload\n    def __new__(cls, o: Iterable[SupportsIndex] | SupportsIndex | SupportsBytes | ReadableBuffer, /) -> Self: ...\n    @overload\n    def __new__(cls, string: str, /, encoding: str, errors: str = 'strict') -> Self: ...\n    @overload\n    def __new__(cls) -> Self: ...\n    def capitalize(self) -> bytes: ...\n    def center(self, width: SupportsIndex, fillchar: bytes = b' ', /) -> bytes: ...\n    def count(\n        self,\n        sub: ReadableBuffer | SupportsIndex,\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> int: ...\n    def decode(self, encoding: str = 'utf-8', errors: str = 'strict') -> str: ...\n    def endswith(\n        self,\n        suffix: ReadableBuffer | tuple[ReadableBuffer, ...],\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> bool: ...\n    def expandtabs(self, tabsize: SupportsIndex = 8) -> bytes: ...\n    def find(\n        self,\n        sub: ReadableBuffer | SupportsIndex,\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> int: ...\n    def hex(self, sep: str | bytes = ..., bytes_per_sep: SupportsIndex = 1) -> str: ...\n    def index(\n        self,\n        sub: ReadableBuffer | SupportsIndex,\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> int: ...\n    def isalnum(self) -> bool: ...\n    def isalpha(self) -> bool: ...\n    def isascii(self) -> bool: ...\n    def isdigit(self) -> bool: ...\n    def islower(self) -> bool: ...\n    def isspace(self) -> bool: ...\n    def istitle(self) -> bool: ...\n    def isupper(self) -> bool: ...\n    def join(self, iterable_of_bytes: Iterable[ReadableBuffer], /) -> bytes: ...\n    def ljust(self, width: SupportsIndex, fillchar: bytes | bytearray = b' ', /) -> bytes: ...\n    def lower(self) -> bytes: ...\n    def lstrip(self, bytes: ReadableBuffer | None = None, /) -> bytes: ...\n    def partition(self, sep: ReadableBuffer, /) -> tuple[bytes, bytes, bytes]: ...\n    def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytes: ...\n    def removeprefix(self, prefix: ReadableBuffer, /) -> bytes: ...\n    def removesuffix(self, suffix: ReadableBuffer, /) -> bytes: ...\n    def rfind(\n        self,\n        sub: ReadableBuffer | SupportsIndex,\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> int: ...\n    def rindex(\n        self,\n        sub: ReadableBuffer | SupportsIndex,\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> int: ...\n    def rjust(self, width: SupportsIndex, fillchar: bytes | bytearray = b' ', /) -> bytes: ...\n    def rpartition(self, sep: ReadableBuffer, /) -> tuple[bytes, bytes, bytes]: ...\n    def rsplit(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: ...\n    def rstrip(self, bytes: ReadableBuffer | None = None, /) -> bytes: ...\n    def split(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: ...\n    def splitlines(self, keepends: bool = False) -> list[bytes]: ...\n    def startswith(\n        self,\n        prefix: ReadableBuffer | tuple[ReadableBuffer, ...],\n        start: SupportsIndex | None = None,\n        end: SupportsIndex | None = None,\n        /,\n    ) -> bool: ...\n    def strip(self, bytes: ReadableBuffer | None = None, /) -> bytes: ...\n    def swapcase(self) -> bytes: ...\n    def title(self) -> bytes: ...\n    def translate(self, table: ReadableBuffer | None, /, delete: ReadableBuffer = b'') -> bytes: ...\n    def upper(self) -> bytes: ...\n    def zfill(self, width: SupportsIndex, /) -> bytes: ...\n    if sys.version_info >= (3, 14):\n        @classmethod\n        def fromhex(cls, string: str | ReadableBuffer, /) -> Self: ...\n    else:\n        @classmethod\n        def fromhex(cls, string: str, /) -> Self: ...\n\n    @staticmethod\n    def maketrans(frm: ReadableBuffer, to: ReadableBuffer, /) -> bytes: ...\n    def __len__(self) -> int: ...\n    def __iter__(self) -> Iterator[int]: ...\n    def __hash__(self) -> int: ...\n    @overload\n    def __getitem__(self, key: SupportsIndex, /) -> int: ...\n    @overload\n    def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytes: ...\n    def __add__(self, value: ReadableBuffer, /) -> bytes: ...\n    def __mul__(self, value: SupportsIndex, /) -> bytes: ...\n    def __rmul__(self, value: SupportsIndex, /) -> bytes: ...\n    def __mod__(self, value: Any, /) -> bytes: ...\n    def __contains__(self, key: SupportsIndex | ReadableBuffer, /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __ne__(self, value: object, /) -> bool: ...\n    def __lt__(self, value: bytes, /) -> bool: ...\n    def __le__(self, value: bytes, /) -> bool: ...\n    def __gt__(self, value: bytes, /) -> bool: ...\n    def __ge__(self, value: bytes, /) -> bool: ...\n    def __getnewargs__(self) -> tuple[bytes]: ...\n    if sys.version_info >= (3, 11):\n        def __bytes__(self) -> bytes: ...\n\n    def __buffer__(self, flags: int, /) -> memoryview: ...\n\n_IntegerFormats: TypeAlias = Literal[\n    'b',\n    'B',\n    '@b',\n    '@B',\n    'h',\n    'H',\n    '@h',\n    '@H',\n    'i',\n    'I',\n    '@i',\n    '@I',\n    'l',\n    'L',\n    '@l',\n    '@L',\n    'q',\n    'Q',\n    '@q',\n    '@Q',\n    'P',\n    '@P',\n]\n\n@final\nclass bool(int):\n    def __new__(cls, o: object = False, /) -> Self: ...\n    @overload\n    def __and__(self, value: bool, /) -> bool: ...\n    @overload\n    def __and__(self, value: int, /) -> int: ...\n    @overload\n    def __or__(self, value: bool, /) -> bool: ...\n    @overload\n    def __or__(self, value: int, /) -> int: ...\n    @overload\n    def __xor__(self, value: bool, /) -> bool: ...\n    @overload\n    def __xor__(self, value: int, /) -> int: ...\n    @overload\n    def __rand__(self, value: bool, /) -> bool: ...\n    @overload\n    def __rand__(self, value: int, /) -> int: ...\n    @overload\n    def __ror__(self, value: bool, /) -> bool: ...\n    @overload\n    def __ror__(self, value: int, /) -> int: ...\n    @overload\n    def __rxor__(self, value: bool, /) -> bool: ...\n    @overload\n    def __rxor__(self, value: int, /) -> int: ...\n    def __getnewargs__(self) -> tuple[int]: ...\n    @deprecated('Will throw an error in Python 3.16. Use `not` for logical negation of bools instead.')\n    def __invert__(self) -> int: ...\n\n@final\nclass slice(Generic[_StartT_co, _StopT_co, _StepT_co]):\n    @property\n    def start(self) -> _StartT_co: ...\n    @property\n    def step(self) -> _StepT_co: ...\n    @property\n    def stop(self) -> _StopT_co: ...\n    @overload\n    def __new__(cls, start: None, stop: None = None, step: None = None, /) -> slice[Any, Any, Any]: ...\n    @overload\n    def __new__(cls, stop: _T2, /) -> slice[Any, _T2, Any]: ...\n    @overload\n    def __new__(cls, start: _T1, stop: None, step: None = None, /) -> slice[_T1, Any, Any]: ...\n    @overload\n    def __new__(cls, start: None, stop: _T2, step: None = None, /) -> slice[Any, _T2, Any]: ...\n    @overload\n    def __new__(cls, start: _T1, stop: _T2, step: None = None, /) -> slice[_T1, _T2, Any]: ...\n    @overload\n    def __new__(cls, start: None, stop: None, step: _T3, /) -> slice[Any, Any, _T3]: ...\n    @overload\n    def __new__(cls, start: _T1, stop: None, step: _T3, /) -> slice[_T1, Any, _T3]: ...\n    @overload\n    def __new__(cls, start: None, stop: _T2, step: _T3, /) -> slice[Any, _T2, _T3]: ...\n    @overload\n    def __new__(cls, start: _T1, stop: _T2, step: _T3, /) -> slice[_T1, _T2, _T3]: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    if sys.version_info >= (3, 12):\n        def __hash__(self) -> int: ...\n    else:\n        __hash__: ClassVar[None]\n\n    def indices(self, len: SupportsIndex, /) -> tuple[int, int, int]: ...\n\n@disjoint_base\nclass tuple(Sequence[_T_co]):\n    def __new__(cls, iterable: Iterable[_T_co] = (), /) -> Self: ...\n    def __len__(self) -> int: ...\n    def __contains__(self, key: object, /) -> bool: ...\n    @overload\n    def __getitem__(self, key: SupportsIndex, /) -> _T_co: ...\n    @overload\n    def __getitem__(self, key: slice[SupportsIndex | None], /) -> tuple[_T_co, ...]: ...\n    def __iter__(self) -> Iterator[_T_co]: ...\n    def __lt__(self, value: tuple[_T_co, ...], /) -> bool: ...\n    def __le__(self, value: tuple[_T_co, ...], /) -> bool: ...\n    def __gt__(self, value: tuple[_T_co, ...], /) -> bool: ...\n    def __ge__(self, value: tuple[_T_co, ...], /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    @overload\n    def __add__(self, value: tuple[_T_co, ...], /) -> tuple[_T_co, ...]: ...\n    @overload\n    def __add__(self, value: tuple[_T, ...], /) -> tuple[_T_co | _T, ...]: ...\n    def __mul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ...\n    def __rmul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ...\n    def count(self, value: Any, /) -> int: ...\n    def index(self, value: Any, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n@disjoint_base\nclass list(MutableSequence[_T]):\n    @overload\n    def __init__(self) -> None: ...\n    @overload\n    def __init__(self, iterable: Iterable[_T], /) -> None: ...\n    def copy(self) -> list[_T]: ...\n    def append(self, object: _T, /) -> None: ...\n    def extend(self, iterable: Iterable[_T], /) -> None: ...\n    def pop(self, index: SupportsIndex = -1, /) -> _T: ...\n    def index(self, value: _T, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ...\n    def count(self, value: _T, /) -> int: ...\n    def insert(self, index: SupportsIndex, object: _T, /) -> None: ...\n    def remove(self, value: _T, /) -> None: ...\n    @overload\n    def sort(self: list[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ...\n    @overload\n    def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ...\n    def __len__(self) -> int: ...\n    def __iter__(self) -> Iterator[_T]: ...\n    __hash__: ClassVar[None]\n\n    @overload\n    def __getitem__(self, i: SupportsIndex, /) -> _T: ...\n    @overload\n    def __getitem__(self, s: slice[SupportsIndex | None], /) -> list[_T]: ...\n    @overload\n    def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ...\n    @overload\n    def __setitem__(self, key: slice[SupportsIndex | None], value: Iterable[_T], /) -> None: ...\n    def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: ...\n    @overload\n    def __add__(self, value: list[_T], /) -> list[_T]: ...\n    @overload\n    def __add__(self, value: list[_S], /) -> list[_S | _T]: ...\n    def __iadd__(self, value: Iterable[_T], /) -> Self: ...\n    def __mul__(self, value: SupportsIndex, /) -> list[_T]: ...\n    def __rmul__(self, value: SupportsIndex, /) -> list[_T]: ...\n    def __imul__(self, value: SupportsIndex, /) -> Self: ...\n    def __contains__(self, key: object, /) -> bool: ...\n    def __reversed__(self) -> Iterator[_T]: ...\n    def __gt__(self, value: list[_T], /) -> bool: ...\n    def __ge__(self, value: list[_T], /) -> bool: ...\n    def __lt__(self, value: list[_T], /) -> bool: ...\n    def __le__(self, value: list[_T], /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n@disjoint_base\nclass dict(MutableMapping[_KT, _VT]):\n    @overload\n    def __init__(self, /) -> None: ...\n    @overload\n    def __init__(self: dict[str, _VT], /, **kwargs: _VT) -> None: ...\n    @overload\n    def __init__(self, map: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ...\n    @overload\n    def __init__(self: dict[str, _VT], map: SupportsKeysAndGetItem[str, _VT], /, **kwargs: _VT) -> None: ...\n    @overload\n    def __init__(self, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ...\n    @overload\n    def __init__(self: dict[str, _VT], iterable: Iterable[tuple[str, _VT]], /, **kwargs: _VT) -> None: ...\n    @overload\n    def __init__(self: dict[str, str], iterable: Iterable[list[str]], /) -> None: ...\n    @overload\n    def __init__(self: dict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ...\n    def __new__(cls, /, *args: Any, **kwargs: Any) -> Self: ...\n    def copy(self) -> dict[_KT, _VT]: ...\n    def keys(self) -> dict_keys[_KT, _VT]: ...\n    def values(self) -> dict_values[_KT, _VT]: ...\n    def items(self) -> dict_items[_KT, _VT]: ...\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: None = None, /) -> dict[_T, Any | None]: ...\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ...\n    @overload\n    def get(self, key: _KT, default: None = None, /) -> _VT | None: ...\n    @overload\n    def get(self, key: _KT, default: _VT, /) -> _VT: ...\n    @overload\n    def get(self, key: _KT, default: _T, /) -> _VT | _T: ...\n    @overload\n    def pop(self, key: _KT, /) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _VT, /) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _T, /) -> _VT | _T: ...\n    def __len__(self) -> int: ...\n    def __getitem__(self, key: _KT, /) -> _VT: ...\n    def __setitem__(self, key: _KT, value: _VT, /) -> None: ...\n    def __delitem__(self, key: _KT, /) -> None: ...\n    def __iter__(self) -> Iterator[_KT]: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __reversed__(self) -> Iterator[_KT]: ...\n    __hash__: ClassVar[None]\n\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n    @overload\n    def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ...\n    @overload\n    def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ...\n    @overload\n    def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ...\n    @overload\n    def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ...\n    @overload\n    def __ior__(self, value: SupportsKeysAndGetItem[_KT, _VT], /) -> Self: ...\n    @overload\n    def __ior__(self, value: Iterable[tuple[_KT, _VT]], /) -> Self: ...\n\n@disjoint_base\nclass set(MutableSet[_T]):\n    @overload\n    def __init__(self) -> None: ...\n    @overload\n    def __init__(self, iterable: Iterable[_T], /) -> None: ...\n    def add(self, element: _T, /) -> None: ...\n    def copy(self) -> set[_T]: ...\n    def difference(self, *s: Iterable[object]) -> set[_T]: ...\n    def difference_update(self, *s: Iterable[object]) -> None: ...\n    def discard(self, element: object, /) -> None: ...\n    def intersection(self, *s: Iterable[object]) -> set[_T]: ...\n    def intersection_update(self, *s: Iterable[object]) -> None: ...\n    def isdisjoint(self, s: Iterable[object], /) -> bool: ...\n    def issubset(self, s: Iterable[object], /) -> bool: ...\n    def issuperset(self, s: Iterable[object], /) -> bool: ...\n    def remove(self, element: _T, /) -> None: ...\n    def symmetric_difference(self, s: Iterable[_S], /) -> set[_T | _S]: ...\n    def symmetric_difference_update(self, s: Iterable[_T], /) -> None: ...\n    def union(self, *s: Iterable[_S]) -> set[_T | _S]: ...\n    def update(self, *s: Iterable[_T]) -> None: ...\n    def __len__(self) -> int: ...\n    def __contains__(self, o: object, /) -> bool: ...\n    def __iter__(self) -> Iterator[_T]: ...\n    def __and__(self, value: AbstractSet[object], /) -> set[_T]: ...\n    def __iand__(self, value: AbstractSet[object], /) -> Self: ...\n    def __or__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ...\n    def __ior__(self, value: AbstractSet[_T], /) -> Self: ...\n    def __sub__(self, value: AbstractSet[object], /) -> set[_T]: ...\n    def __isub__(self, value: AbstractSet[object], /) -> Self: ...\n    def __xor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ...\n    def __ixor__(self, value: AbstractSet[_T], /) -> Self: ...\n    def __le__(self, value: AbstractSet[object], /) -> bool: ...\n    def __lt__(self, value: AbstractSet[object], /) -> bool: ...\n    def __ge__(self, value: AbstractSet[object], /) -> bool: ...\n    def __gt__(self, value: AbstractSet[object], /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    __hash__: ClassVar[None]\n\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n@disjoint_base\nclass frozenset(AbstractSet[_T_co]):\n    @overload\n    def __new__(cls) -> Self: ...\n    @overload\n    def __new__(cls, iterable: Iterable[_T_co], /) -> Self: ...\n    def copy(self) -> frozenset[_T_co]: ...\n    def difference(self, *s: Iterable[object]) -> frozenset[_T_co]: ...\n    def intersection(self, *s: Iterable[object]) -> frozenset[_T_co]: ...\n    def isdisjoint(self, s: Iterable[object], /) -> bool: ...\n    def issubset(self, s: Iterable[object], /) -> bool: ...\n    def issuperset(self, s: Iterable[object], /) -> bool: ...\n    def symmetric_difference(self, s: Iterable[_S], /) -> frozenset[_T_co | _S]: ...\n    def union(self, *s: Iterable[_S]) -> frozenset[_T_co | _S]: ...\n    def __len__(self) -> int: ...\n    def __contains__(self, o: object, /) -> bool: ...\n    def __iter__(self) -> Iterator[_T_co]: ...\n    def __and__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ...\n    def __or__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ...\n    def __sub__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ...\n    def __xor__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ...\n    def __le__(self, value: AbstractSet[object], /) -> bool: ...\n    def __lt__(self, value: AbstractSet[object], /) -> bool: ...\n    def __ge__(self, value: AbstractSet[object], /) -> bool: ...\n    def __gt__(self, value: AbstractSet[object], /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n@disjoint_base\nclass enumerate(Generic[_T]):\n    def __new__(cls, iterable: Iterable[_T], start: int = 0) -> Self: ...\n    def __iter__(self) -> Self: ...\n    def __next__(self) -> tuple[int, _T]: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n@final\nclass range(Sequence[int]):\n    @property\n    def start(self) -> int: ...\n    @property\n    def stop(self) -> int: ...\n    @property\n    def step(self) -> int: ...\n    @overload\n    def __new__(cls, stop: SupportsIndex, /) -> Self: ...\n    @overload\n    def __new__(cls, start: SupportsIndex, stop: SupportsIndex, step: SupportsIndex = 1, /) -> Self: ...\n    def count(self, value: int, /) -> int: ...\n    def index(self, value: int, /) -> int: ...\n    def __len__(self) -> int: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    def __contains__(self, key: object, /) -> bool: ...\n    def __iter__(self) -> Iterator[int]: ...\n    @overload\n    def __getitem__(self, key: SupportsIndex, /) -> int: ...\n    @overload\n    def __getitem__(self, key: slice[SupportsIndex | None], /) -> range: ...\n    def __reversed__(self) -> Iterator[int]: ...\n\n@disjoint_base\nclass property:\n    fget: Callable[[Any], Any] | None\n    fset: Callable[[Any, Any], None] | None\n    fdel: Callable[[Any], None] | None\n    __isabstractmethod__: bool\n    if sys.version_info >= (3, 13):\n        __name__: str\n\n    def __init__(\n        self,\n        fget: Callable[[Any], Any] | None = None,\n        fset: Callable[[Any, Any], None] | None = None,\n        fdel: Callable[[Any], None] | None = None,\n        doc: str | None = None,\n    ) -> None: ...\n    def getter(self, fget: Callable[[Any], Any], /) -> property: ...\n    def setter(self, fset: Callable[[Any, Any], None], /) -> property: ...\n    def deleter(self, fdel: Callable[[Any], None], /) -> property: ...\n    @overload\n    def __get__(self, instance: None, owner: type, /) -> Self: ...\n    @overload\n    def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...\n    def __set__(self, instance: Any, value: Any, /) -> None: ...\n    def __delete__(self, instance: Any, /) -> None: ...\n\ndef abs(x: SupportsAbs[_T], /) -> _T: ...\ndef all(iterable: Iterable[object], /) -> bool: ...\ndef any(iterable: Iterable[object], /) -> bool: ...\ndef bin(number: SupportsIndex, /) -> str: ...\ndef chr(i: SupportsIndex, /) -> str: ...\n\nif sys.version_info >= (3, 10):\n    @type_check_only\n    class _SupportsSynchronousAnext(Protocol[_AwaitableT_co]):\n        def __anext__(self) -> _AwaitableT_co: ...\n\ncopyright: _sitebuiltins._Printer\ncredits: _sitebuiltins._Printer\n\n@overload\ndef divmod(x: SupportsDivMod[_T_contra, _T_co], y: _T_contra, /) -> _T_co: ...\n@overload\ndef divmod(x: _T_contra, y: SupportsRDivMod[_T_contra, _T_co], /) -> _T_co: ...\n\nexit: _sitebuiltins.Quitter\n\ndef hash(obj: object, /) -> int: ...\n\nhelp: _sitebuiltins._Helper\n\ndef hex(number: SupportsIndex, /) -> str: ...\ndef id(obj: object, /) -> int: ...\n@type_check_only\nclass _GetItemIterable(Protocol[_T_co]):\n    def __getitem__(self, i: int, /) -> _T_co: ...\n\nif sys.version_info >= (3, 10):\n    _ClassInfo: TypeAlias = type | types.UnionType | tuple[_ClassInfo, ...]\nelse:\n    _ClassInfo: TypeAlias = type | tuple[_ClassInfo, ...]\n\ndef isinstance(obj: object, class_or_tuple: _ClassInfo, /) -> bool: ...\ndef len(obj: Sized, /) -> int: ...\n\nlicense: _sitebuiltins._Printer\n\n@overload\ndef max(\n    arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, key: None = None\n) -> SupportsRichComparisonT: ...\n@overload\ndef max(arg1: _T, arg2: _T, /, *_args: _T, key: Callable[[_T], SupportsRichComparison]) -> _T: ...\n@overload\ndef max(iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None) -> SupportsRichComparisonT: ...\n@overload\ndef max(iterable: Iterable[_T], /, *, key: Callable[[_T], SupportsRichComparison]) -> _T: ...\n@overload\ndef max(\n    iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T\n) -> SupportsRichComparisonT | _T: ...\n@overload\ndef max(iterable: Iterable[_T1], /, *, key: Callable[[_T1], SupportsRichComparison], default: _T2) -> _T1 | _T2: ...\n@overload\ndef min(\n    arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, key: None = None\n) -> SupportsRichComparisonT: ...\n@overload\ndef min(arg1: _T, arg2: _T, /, *_args: _T, key: Callable[[_T], SupportsRichComparison]) -> _T: ...\n@overload\ndef min(iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None) -> SupportsRichComparisonT: ...\n@overload\ndef min(iterable: Iterable[_T], /, *, key: Callable[[_T], SupportsRichComparison]) -> _T: ...\n@overload\ndef min(\n    iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T\n) -> SupportsRichComparisonT | _T: ...\n@overload\ndef min(iterable: Iterable[_T1], /, *, key: Callable[[_T1], SupportsRichComparison], default: _T2) -> _T1 | _T2: ...\ndef oct(number: SupportsIndex, /) -> str: ...\n\n_Opener: TypeAlias = Callable[[str, int], int]\n\ndef ord(c: str | bytes | bytearray, /) -> int: ...\n@type_check_only\nclass _SupportsWriteAndFlush(SupportsWrite[_T_contra], SupportsFlush, Protocol[_T_contra]): ...\n\n@overload\ndef print(\n    *values: object,\n    sep: str | None = ' ',\n    end: str | None = '\\n',\n    file: SupportsWrite[str] | None = None,\n    flush: Literal[False] = False,\n) -> None: ...\n@overload\ndef print(\n    *values: object,\n    sep: str | None = ' ',\n    end: str | None = '\\n',\n    file: _SupportsWriteAndFlush[str] | None = None,\n    flush: bool,\n) -> None: ...\n\n_E_contra = TypeVar('_E_contra', contravariant=True)\n_M_contra = TypeVar('_M_contra', contravariant=True)\n\n@type_check_only\nclass _SupportsPow2(Protocol[_E_contra, _T_co]):\n    def __pow__(self, other: _E_contra, /) -> _T_co: ...\n\n@type_check_only\nclass _SupportsPow3NoneOnly(Protocol[_E_contra, _T_co]):\n    def __pow__(self, other: _E_contra, modulo: None = None, /) -> _T_co: ...\n\n@type_check_only\nclass _SupportsPow3(Protocol[_E_contra, _M_contra, _T_co]):\n    def __pow__(self, other: _E_contra, modulo: _M_contra, /) -> _T_co: ...\n\n_SupportsSomeKindOfPow = _SupportsPow2[Any, Any] | _SupportsPow3NoneOnly[Any, Any] | _SupportsPow3[Any, Any, Any]\n\n@overload\ndef pow(base: int, exp: int, mod: int) -> int: ...\n@overload\ndef pow(base: int, exp: Literal[0], mod: None = None) -> Literal[1]: ...\n@overload\ndef pow(base: int, exp: _PositiveInteger, mod: None = None) -> int: ...\n@overload\ndef pow(base: int, exp: _NegativeInteger, mod: None = None) -> float: ...\n@overload\ndef pow(base: int, exp: int, mod: None = None) -> Any: ...\n@overload\ndef pow(base: _PositiveInteger, exp: float, mod: None = None) -> float: ...\n@overload\ndef pow(base: _NegativeInteger, exp: float, mod: None = None) -> complex: ...\n@overload\ndef pow(base: float, exp: int, mod: None = None) -> float: ...\n@overload\ndef pow(base: float, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> Any: ...\n@overload\ndef pow(base: complex, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> complex: ...\n@overload\ndef pow(base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...\n@overload\ndef pow(base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...\n@overload\ndef pow(base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co: ...\n@overload\ndef pow(base: _SupportsSomeKindOfPow, exp: float, mod: None = None) -> Any: ...\n@overload\ndef pow(base: _SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex: ...\n\nquit: _sitebuiltins.Quitter\n\n@disjoint_base\nclass reversed(Generic[_T]):\n    @overload\n    def __new__(cls, sequence: Reversible[_T], /) -> Iterator[_T]: ...\n    @overload\n    def __new__(cls, sequence: SupportsLenAndGetItem[_T], /) -> Iterator[_T]: ...\n    def __iter__(self) -> Self: ...\n    def __next__(self) -> _T: ...\n    def __length_hint__(self) -> int: ...\n\ndef repr(obj: object, /) -> str: ...\n@type_check_only\nclass _SupportsRound1(Protocol[_T_co]):\n    def __round__(self) -> _T_co: ...\n\n@type_check_only\nclass _SupportsRound2(Protocol[_T_co]):\n    def __round__(self, ndigits: int, /) -> _T_co: ...\n\n@overload\ndef round(number: _SupportsRound1[_T], ndigits: None = None) -> _T: ...\n@overload\ndef round(number: _SupportsRound2[_T], ndigits: SupportsIndex) -> _T: ...\n@overload\ndef sorted(\n    iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, reverse: bool = False\n) -> list[SupportsRichComparisonT]: ...\n@overload\ndef sorted(\n    iterable: Iterable[_T], /, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False\n) -> list[_T]: ...\n\n_AddableT1 = TypeVar('_AddableT1', bound=SupportsAdd[Any, Any])\n_AddableT2 = TypeVar('_AddableT2', bound=SupportsAdd[Any, Any])\n\n@type_check_only\nclass _SupportsSumWithNoDefaultGiven(SupportsAdd[Any, Any], SupportsRAdd[int, Any], Protocol): ...\n\n_SupportsSumNoDefaultT = TypeVar('_SupportsSumNoDefaultT', bound=_SupportsSumWithNoDefaultGiven)\n\n@overload\ndef sum(iterable: Iterable[bool | _LiteralInteger], /, start: int = 0) -> int: ...\n@overload\ndef sum(iterable: Iterable[_SupportsSumNoDefaultT], /) -> _SupportsSumNoDefaultT | Literal[0]: ...\n@overload\ndef sum(iterable: Iterable[_AddableT1], /, start: _AddableT2) -> _AddableT1 | _AddableT2: ...\n@disjoint_base\nclass zip(Generic[_T_co]):\n    if sys.version_info >= (3, 10):\n        @overload\n        def __new__(cls, *, strict: bool = False) -> zip[Any]: ...\n        @overload\n        def __new__(cls, iter1: Iterable[_T1], /, *, strict: bool = False) -> zip[tuple[_T1]]: ...\n        @overload\n        def __new__(\n            cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /, *, strict: bool = False\n        ) -> zip[tuple[_T1, _T2]]: ...\n        @overload\n        def __new__(\n            cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /, *, strict: bool = False\n        ) -> zip[tuple[_T1, _T2, _T3]]: ...\n        @overload\n        def __new__(\n            cls,\n            iter1: Iterable[_T1],\n            iter2: Iterable[_T2],\n            iter3: Iterable[_T3],\n            iter4: Iterable[_T4],\n            /,\n            *,\n            strict: bool = False,\n        ) -> zip[tuple[_T1, _T2, _T3, _T4]]: ...\n        @overload\n        def __new__(\n            cls,\n            iter1: Iterable[_T1],\n            iter2: Iterable[_T2],\n            iter3: Iterable[_T3],\n            iter4: Iterable[_T4],\n            iter5: Iterable[_T5],\n            /,\n            *,\n            strict: bool = False,\n        ) -> zip[tuple[_T1, _T2, _T3, _T4, _T5]]: ...\n        @overload\n        def __new__(\n            cls,\n            iter1: Iterable[Any],\n            iter2: Iterable[Any],\n            iter3: Iterable[Any],\n            iter4: Iterable[Any],\n            iter5: Iterable[Any],\n            iter6: Iterable[Any],\n            /,\n            *iterables: Iterable[Any],\n            strict: bool = False,\n        ) -> zip[tuple[Any, ...]]: ...\n    else:\n        @overload\n        def __new__(cls) -> zip[Any]: ...\n        @overload\n        def __new__(cls, iter1: Iterable[_T1], /) -> zip[tuple[_T1]]: ...\n        @overload\n        def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /) -> zip[tuple[_T1, _T2]]: ...\n        @overload\n        def __new__(\n            cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /\n        ) -> zip[tuple[_T1, _T2, _T3]]: ...\n        @overload\n        def __new__(\n            cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /\n        ) -> zip[tuple[_T1, _T2, _T3, _T4]]: ...\n        @overload\n        def __new__(\n            cls,\n            iter1: Iterable[_T1],\n            iter2: Iterable[_T2],\n            iter3: Iterable[_T3],\n            iter4: Iterable[_T4],\n            iter5: Iterable[_T5],\n            /,\n        ) -> zip[tuple[_T1, _T2, _T3, _T4, _T5]]: ...\n        @overload\n        def __new__(\n            cls,\n            iter1: Iterable[Any],\n            iter2: Iterable[Any],\n            iter3: Iterable[Any],\n            iter4: Iterable[Any],\n            iter5: Iterable[Any],\n            iter6: Iterable[Any],\n            /,\n            *iterables: Iterable[Any],\n        ) -> zip[tuple[Any, ...]]: ...\n\n    def __iter__(self) -> Self: ...\n    def __next__(self) -> _T_co: ...\n\nif sys.version_info >= (3, 10):\n    from types import EllipsisType, NotImplementedType\n\n    ellipsis = EllipsisType\n    Ellipsis: EllipsisType\n    NotImplemented: NotImplementedType\nelse:\n    Ellipsis: ellipsis\n\n    @final\n    @type_check_only\n    class _NotImplementedType(Any): ...\n\n    NotImplemented: _NotImplementedType\n\n@disjoint_base\nclass BaseException:\n    args: tuple[Any, ...]\n    __cause__: BaseException | None\n    __context__: BaseException | None\n    __suppress_context__: bool\n    __traceback__: TracebackType | None\n\n    def __init__(self, *args: object) -> None: ...\n    def __new__(cls, *args: Any, **kwds: Any) -> Self: ...\n    def __setstate__(self, state: dict[str, Any] | None, /) -> None: ...\n    def with_traceback(self, tb: TracebackType | None, /) -> Self: ...\n    def __str__(self) -> str: ...\n    def __repr__(self) -> str: ...\n    if sys.version_info >= (3, 11):\n        __notes__: list[str]\n\n        def add_note(self, note: str, /) -> None: ...\n\nclass KeyboardInterrupt(BaseException): ...\n\n@disjoint_base\nclass SystemExit(BaseException):\n    code: sys._ExitCode\n\nclass Exception(BaseException): ...\n\n@disjoint_base\nclass StopIteration(Exception):\n    value: Any\n\n@disjoint_base\nclass OSError(Exception):\n    errno: int | None\n    strerror: str | None\n    filename: Any\n    filename2: Any\n    if sys.platform == 'win32':\n        winerror: int\n\nEnvironmentError = OSError\nIOError = OSError\nif sys.platform == 'win32':\n    WindowsError = OSError\n\nclass ArithmeticError(Exception): ...\nclass AssertionError(Exception): ...\n\nif sys.version_info >= (3, 10):\n    @disjoint_base\n    class AttributeError(Exception):\n        def __init__(self, *args: object, name: str | None = None, obj: object = None) -> None: ...\n        name: str | None\n        obj: object\n\nelse:\n    class AttributeError(Exception): ...\n\nclass LookupError(Exception): ...\nclass MemoryError(Exception): ...\n\nif sys.version_info >= (3, 10):\n    @disjoint_base\n    class NameError(Exception):\n        def __init__(self, *args: object, name: str | None = None) -> None: ...\n        name: str | None\n\nelse:\n    class NameError(Exception): ...\n\nclass RuntimeError(Exception): ...\n\n@disjoint_base\nclass SyntaxError(Exception):\n    msg: str\n    filename: str | None\n    lineno: int | None\n    offset: int | None\n    text: str | None\n    print_file_and_line: None\n    if sys.version_info >= (3, 10):\n        end_lineno: int | None\n        end_offset: int | None\n\n    @overload\n    def __init__(self) -> None: ...\n    @overload\n    def __init__(self, msg: object, /) -> None: ...\n    @overload\n    def __init__(self, msg: str, info: tuple[str | None, int | None, int | None, str | None], /) -> None: ...\n    if sys.version_info >= (3, 10):\n        @overload\n        def __init__(\n            self, msg: str, info: tuple[str | None, int | None, int | None, str | None, int | None, int | None], /\n        ) -> None: ...\n\nclass TypeError(Exception): ...\nclass ValueError(Exception): ...\nclass OverflowError(ArithmeticError): ...\nclass ZeroDivisionError(ArithmeticError): ...\nclass IndexError(LookupError): ...\nclass KeyError(LookupError): ...\nclass TimeoutError(OSError): ...\nclass NotImplementedError(RuntimeError): ...\nclass RecursionError(RuntimeError): ...\n\nif sys.version_info >= (3, 11):\n    _BaseExceptionT_co = TypeVar('_BaseExceptionT_co', bound=BaseException, covariant=True, default=BaseException)\n    _BaseExceptionT = TypeVar('_BaseExceptionT', bound=BaseException)\n    _ExceptionT_co = TypeVar('_ExceptionT_co', bound=Exception, covariant=True, default=Exception)\n    _ExceptionT = TypeVar('_ExceptionT', bound=Exception)\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/collections/__init__.pyi",
    "content": "import sys\nfrom _collections_abc import dict_items, dict_keys, dict_values\nfrom types import GenericAlias\nfrom typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only\n\nfrom _typeshed import SupportsItems, SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT\nfrom typing_extensions import Self, disjoint_base\n\nif sys.version_info >= (3, 10):\n    from collections.abc import (\n        Callable,\n        ItemsView,\n        Iterable,\n        Iterator,\n        KeysView,\n        Mapping,\n        MutableMapping,\n        MutableSequence,\n        Sequence,\n        ValuesView,\n    )\nelse:\n    from _collections_abc import *\n\n__all__ = [\n    'ChainMap',\n    'Counter',\n    'OrderedDict',\n    'UserDict',\n    'UserList',\n    'UserString',\n    'defaultdict',\n    'deque',\n    'namedtuple',\n]\n\n_S = TypeVar('_S')\n_T = TypeVar('_T')\n_T1 = TypeVar('_T1')\n_T2 = TypeVar('_T2')\n_KT = TypeVar('_KT')\n_VT = TypeVar('_VT')\n_KT_co = TypeVar('_KT_co', covariant=True)\n_VT_co = TypeVar('_VT_co', covariant=True)\n\n# namedtuple is special-cased in the type checker; the initializer is ignored.\ndef namedtuple(\n    typename: str,\n    field_names: str | Iterable[str],\n    *,\n    rename: bool = False,\n    module: str | None = None,\n    defaults: Iterable[Any] | None = None,\n) -> type[tuple[Any, ...]]: ...\n\nclass UserDict(MutableMapping[_KT, _VT]):\n    data: dict[_KT, _VT]\n    # __init__ should be kept roughly in line with `dict.__init__`, which has the same semantics\n    @overload\n    def __init__(self, dict: None = None, /) -> None: ...\n    @overload\n    def __init__(\n        self: UserDict[str, _VT],\n        dict: None = None,\n        /,\n        **kwargs: _VT,  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n    ) -> None: ...\n    @overload\n    def __init__(self, dict: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ...\n    @overload\n    def __init__(\n        self: UserDict[str, _VT],  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n        dict: SupportsKeysAndGetItem[str, _VT],\n        /,\n        **kwargs: _VT,\n    ) -> None: ...\n    @overload\n    def __init__(self, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ...\n    @overload\n    def __init__(\n        self: UserDict[str, _VT],  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n        iterable: Iterable[tuple[str, _VT]],\n        /,\n        **kwargs: _VT,\n    ) -> None: ...\n    @overload\n    def __init__(self: UserDict[str, str], iterable: Iterable[list[str]], /) -> None: ...\n    @overload\n    def __init__(self: UserDict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ...\n    def __len__(self) -> int: ...\n    def __getitem__(self, key: _KT) -> _VT: ...\n    def __setitem__(self, key: _KT, item: _VT) -> None: ...\n    def __delitem__(self, key: _KT) -> None: ...\n    def __iter__(self) -> Iterator[_KT]: ...\n    def __contains__(self, key: object) -> bool: ...\n    def copy(self) -> Self: ...\n    def __copy__(self) -> Self: ...\n\n    # `UserDict.fromkeys` has the same semantics as `dict.fromkeys`, so should be kept in line with `dict.fromkeys`.\n    # TODO: Much like `dict.fromkeys`, the true signature of `UserDict.fromkeys` is inexpressible in the current type system.\n    # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963.\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> UserDict[_T, Any | None]: ...\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: _S) -> UserDict[_T, _S]: ...\n    @overload\n    def __or__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ...\n    @overload\n    def __or__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ...\n    @overload\n    def __ror__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ...\n    @overload\n    def __ror__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ...\n    # UserDict.__ior__ should be kept roughly in line with MutableMapping.update()\n    @overload  # type: ignore[misc]\n    def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ...\n    @overload\n    def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ...\n    if sys.version_info >= (3, 12):\n        @overload\n        def get(self, key: _KT, default: None = None) -> _VT | None: ...\n        @overload\n        def get(self, key: _KT, default: _VT) -> _VT: ...\n        @overload\n        def get(self, key: _KT, default: _T) -> _VT | _T: ...\n\nclass UserList(MutableSequence[_T]):\n    data: list[_T]\n    @overload\n    def __init__(self, initlist: None = None) -> None: ...\n    @overload\n    def __init__(self, initlist: Iterable[_T]) -> None: ...\n    __hash__: ClassVar[None]  # type: ignore[assignment]\n    def __lt__(self, other: list[_T] | UserList[_T]) -> bool: ...\n    def __le__(self, other: list[_T] | UserList[_T]) -> bool: ...\n    def __gt__(self, other: list[_T] | UserList[_T]) -> bool: ...\n    def __ge__(self, other: list[_T] | UserList[_T]) -> bool: ...\n    def __eq__(self, other: object) -> bool: ...\n    def __contains__(self, item: object) -> bool: ...\n    def __len__(self) -> int: ...\n    @overload\n    def __getitem__(self, i: SupportsIndex) -> _T: ...\n    @overload\n    def __getitem__(self, i: slice[SupportsIndex | None]) -> Self: ...\n    @overload\n    def __setitem__(self, i: SupportsIndex, item: _T) -> None: ...\n    @overload\n    def __setitem__(self, i: slice[SupportsIndex | None], item: Iterable[_T]) -> None: ...\n    def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None]) -> None: ...\n    def __add__(self, other: Iterable[_T]) -> Self: ...\n    def __radd__(self, other: Iterable[_T]) -> Self: ...\n    def __iadd__(self, other: Iterable[_T]) -> Self: ...\n    def __mul__(self, n: int) -> Self: ...\n    def __rmul__(self, n: int) -> Self: ...\n    def __imul__(self, n: int) -> Self: ...\n    def append(self, item: _T) -> None: ...\n    def insert(self, i: int, item: _T) -> None: ...\n    def pop(self, i: int = -1) -> _T: ...\n    def remove(self, item: _T) -> None: ...\n    def copy(self) -> Self: ...\n    def __copy__(self) -> Self: ...\n    def count(self, item: _T) -> int: ...\n    # The runtime signature is \"item, *args\", and the arguments are then passed\n    # to `list.index`. In order to give more precise types, we pretend that the\n    # `item` argument is positional-only.\n    def index(self, item: _T, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ...\n    # All arguments are passed to `list.sort` at runtime, so the signature should be kept in line with `list.sort`.\n    @overload\n    def sort(self: UserList[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ...\n    @overload\n    def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ...\n    def extend(self, other: Iterable[_T]) -> None: ...\n\nclass UserString(Sequence[UserString]):\n    data: str\n    def __init__(self, seq: object) -> None: ...\n    def __int__(self) -> int: ...\n    def __float__(self) -> float: ...\n    def __complex__(self) -> complex: ...\n    def __getnewargs__(self) -> tuple[str]: ...\n    def __lt__(self, string: str | UserString) -> bool: ...\n    def __le__(self, string: str | UserString) -> bool: ...\n    def __gt__(self, string: str | UserString) -> bool: ...\n    def __ge__(self, string: str | UserString) -> bool: ...\n    def __eq__(self, string: object) -> bool: ...\n    def __hash__(self) -> int: ...\n    def __contains__(self, char: object) -> bool: ...\n    def __len__(self) -> int: ...\n    def __getitem__(self, index: SupportsIndex | slice[SupportsIndex | None]) -> Self: ...\n    def __iter__(self) -> Iterator[Self]: ...\n    def __reversed__(self) -> Iterator[Self]: ...\n    def __add__(self, other: object) -> Self: ...\n    def __radd__(self, other: object) -> Self: ...\n    def __mul__(self, n: int) -> Self: ...\n    def __rmul__(self, n: int) -> Self: ...\n    def __mod__(self, args: Any) -> Self: ...\n    def __rmod__(self, template: object) -> Self: ...\n    def capitalize(self) -> Self: ...\n    def casefold(self) -> Self: ...\n    def center(self, width: int, *args: Any) -> Self: ...\n    def count(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ...\n    def encode(self: UserString, encoding: str | None = 'utf-8', errors: str | None = 'strict') -> bytes: ...\n    def endswith(self, suffix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ...\n    def expandtabs(self, tabsize: int = 8) -> Self: ...\n    def find(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ...\n    def format(self, *args: Any, **kwds: Any) -> str: ...\n    def format_map(self, mapping: Mapping[str, Any]) -> str: ...\n    def index(self, sub: str, start: int = 0, end: int = sys.maxsize) -> int: ...\n    def isalpha(self) -> bool: ...\n    def isalnum(self) -> bool: ...\n    def isdecimal(self) -> bool: ...\n    def isdigit(self) -> bool: ...\n    def isidentifier(self) -> bool: ...\n    def islower(self) -> bool: ...\n    def isnumeric(self) -> bool: ...\n    def isprintable(self) -> bool: ...\n    def isspace(self) -> bool: ...\n    def istitle(self) -> bool: ...\n    def isupper(self) -> bool: ...\n    def isascii(self) -> bool: ...\n    def join(self, seq: Iterable[str]) -> str: ...\n    def ljust(self, width: int, *args: Any) -> Self: ...\n    def lower(self) -> Self: ...\n    def lstrip(self, chars: str | None = None) -> Self: ...\n    maketrans = str.maketrans\n    def partition(self, sep: str) -> tuple[str, str, str]: ...\n    def removeprefix(self, prefix: str | UserString, /) -> Self: ...\n    def removesuffix(self, suffix: str | UserString, /) -> Self: ...\n    def replace(self, old: str | UserString, new: str | UserString, maxsplit: int = -1) -> Self: ...\n    def rfind(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ...\n    def rindex(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ...\n    def rjust(self, width: int, *args: Any) -> Self: ...\n    def rpartition(self, sep: str) -> tuple[str, str, str]: ...\n    def rstrip(self, chars: str | None = None) -> Self: ...\n    def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...\n    def rsplit(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...\n    def splitlines(self, keepends: bool = False) -> list[str]: ...\n    def startswith(\n        self, prefix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize\n    ) -> bool: ...\n    def strip(self, chars: str | None = None) -> Self: ...\n    def swapcase(self) -> Self: ...\n    def title(self) -> Self: ...\n    def translate(self, *args: Any) -> Self: ...\n    def upper(self) -> Self: ...\n    def zfill(self, width: int) -> Self: ...\n\n@disjoint_base\nclass deque(MutableSequence[_T]):\n    @property\n    def maxlen(self) -> int | None: ...\n    @overload\n    def __init__(self, *, maxlen: int | None = None) -> None: ...\n    @overload\n    def __init__(self, iterable: Iterable[_T], maxlen: int | None = None) -> None: ...\n    def append(self, x: _T, /) -> None: ...\n    def appendleft(self, x: _T, /) -> None: ...\n    def copy(self) -> Self: ...\n    def count(self, x: _T, /) -> int: ...\n    def extend(self, iterable: Iterable[_T], /) -> None: ...\n    def extendleft(self, iterable: Iterable[_T], /) -> None: ...\n    def insert(self, i: int, x: _T, /) -> None: ...\n    def index(self, x: _T, start: int = 0, stop: int = ..., /) -> int: ...\n    def pop(self) -> _T: ...  # type: ignore[override]\n    def popleft(self) -> _T: ...\n    def remove(self, value: _T, /) -> None: ...\n    def rotate(self, n: int = 1, /) -> None: ...\n    def __copy__(self) -> Self: ...\n    def __len__(self) -> int: ...\n    __hash__: ClassVar[None]  # type: ignore[assignment]\n    # These methods of deque don't take slices, unlike MutableSequence, hence the type: ignores\n    def __getitem__(self, key: SupportsIndex, /) -> _T: ...  # type: ignore[override]\n    def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ...  # type: ignore[override]\n    def __delitem__(self, key: SupportsIndex, /) -> None: ...  # type: ignore[override]\n    def __contains__(self, key: object, /) -> bool: ...\n    def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ...\n    def __iadd__(self, value: Iterable[_T], /) -> Self: ...\n    def __add__(self, value: Self, /) -> Self: ...\n    def __mul__(self, value: int, /) -> Self: ...\n    def __imul__(self, value: int, /) -> Self: ...\n    def __lt__(self, value: deque[_T], /) -> bool: ...\n    def __le__(self, value: deque[_T], /) -> bool: ...\n    def __gt__(self, value: deque[_T], /) -> bool: ...\n    def __ge__(self, value: deque[_T], /) -> bool: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\nclass Counter(dict[_T, int], Generic[_T]):\n    @overload\n    def __init__(self, iterable: None = None, /) -> None: ...\n    @overload\n    def __init__(self: Counter[str], iterable: None = None, /, **kwargs: int) -> None: ...\n    @overload\n    def __init__(self, mapping: SupportsKeysAndGetItem[_T, int], /) -> None: ...\n    @overload\n    def __init__(self, iterable: Iterable[_T], /) -> None: ...\n    def copy(self) -> Self: ...\n    def elements(self) -> Iterator[_T]: ...\n    def most_common(self, n: int | None = None) -> list[tuple[_T, int]]: ...\n    @classmethod\n    def fromkeys(cls, iterable: Any, v: int | None = None) -> NoReturn: ...  # type: ignore[override]\n    @overload\n    def subtract(self, iterable: None = None, /) -> None: ...\n    @overload\n    def subtract(self, mapping: Mapping[_T, int], /) -> None: ...\n    @overload\n    def subtract(self, iterable: Iterable[_T], /) -> None: ...\n    # Unlike dict.update(), use Mapping instead of SupportsKeysAndGetItem for the first overload\n    # (source code does an `isinstance(other, Mapping)` check)\n    #\n    # The second overload is also deliberately different to dict.update()\n    # (if it were `Iterable[_T] | Iterable[tuple[_T, int]]`,\n    # the tuples would be added as keys, breaking type safety)\n    @overload  # type: ignore[override]\n    def update(self, m: Mapping[_T, int], /, **kwargs: int) -> None: ...\n    @overload\n    def update(self, iterable: Iterable[_T], /, **kwargs: int) -> None: ...\n    @overload\n    def update(self, iterable: None = None, /, **kwargs: int) -> None: ...\n    def __missing__(self, key: _T) -> int: ...\n    def __delitem__(self, elem: object) -> None: ...\n    if sys.version_info >= (3, 10):\n        def __eq__(self, other: object) -> bool: ...\n        def __ne__(self, other: object) -> bool: ...\n\n    def __add__(self, other: Counter[_S]) -> Counter[_T | _S]: ...\n    def __sub__(self, other: Counter[_T]) -> Counter[_T]: ...\n    def __and__(self, other: Counter[_T]) -> Counter[_T]: ...\n    def __or__(self, other: Counter[_S]) -> Counter[_T | _S]: ...  # type: ignore[override]\n    def __pos__(self) -> Counter[_T]: ...\n    def __neg__(self) -> Counter[_T]: ...\n    # several type: ignores because __iadd__ is supposedly incompatible with __add__, etc.\n    def __iadd__(self, other: SupportsItems[_T, int]) -> Self: ...  # type: ignore[misc]\n    def __isub__(self, other: SupportsItems[_T, int]) -> Self: ...\n    def __iand__(self, other: SupportsItems[_T, int]) -> Self: ...\n    def __ior__(self, other: SupportsItems[_T, int]) -> Self: ...  # type: ignore[override,misc]\n    if sys.version_info >= (3, 10):\n        def total(self) -> int: ...\n        def __le__(self, other: Counter[Any]) -> bool: ...\n        def __lt__(self, other: Counter[Any]) -> bool: ...\n        def __ge__(self, other: Counter[Any]) -> bool: ...\n        def __gt__(self, other: Counter[Any]) -> bool: ...\n\n# The pure-Python implementations of the \"views\" classes\n# These are exposed at runtime in `collections/__init__.py`\nclass _OrderedDictKeysView(KeysView[_KT_co]):\n    def __reversed__(self) -> Iterator[_KT_co]: ...\n\nclass _OrderedDictItemsView(ItemsView[_KT_co, _VT_co]):\n    def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...\n\nclass _OrderedDictValuesView(ValuesView[_VT_co]):\n    def __reversed__(self) -> Iterator[_VT_co]: ...\n\n# The C implementations of the \"views\" classes\n# (At runtime, these are called `odict_keys`, `odict_items` and `odict_values`,\n# but they are not exposed anywhere)\n# pyright doesn't have a specific error code for subclassing error!\n@final\n@type_check_only\nclass _odict_keys(dict_keys[_KT_co, _VT_co]):  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n    def __reversed__(self) -> Iterator[_KT_co]: ...\n\n@final\n@type_check_only\nclass _odict_items(dict_items[_KT_co, _VT_co]):  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n    def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...\n\n@final\n@type_check_only\nclass _odict_values(dict_values[_KT_co, _VT_co]):  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n    def __reversed__(self) -> Iterator[_VT_co]: ...\n\n@disjoint_base\nclass OrderedDict(dict[_KT, _VT]):\n    def popitem(self, last: bool = True) -> tuple[_KT, _VT]: ...\n    def move_to_end(self, key: _KT, last: bool = True) -> None: ...\n    def copy(self) -> Self: ...\n    def __reversed__(self) -> Iterator[_KT]: ...\n    def keys(self) -> _odict_keys[_KT, _VT]: ...\n    def items(self) -> _odict_items[_KT, _VT]: ...\n    def values(self) -> _odict_values[_KT, _VT]: ...\n    # The signature of OrderedDict.fromkeys should be kept in line with `dict.fromkeys`, modulo positional-only differences.\n    # Like dict.fromkeys, its true signature is not expressible in the current type system.\n    # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963.\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> OrderedDict[_T, Any | None]: ...\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: _S) -> OrderedDict[_T, _S]: ...\n    # Keep OrderedDict.setdefault in line with MutableMapping.setdefault, modulo positional-only differences.\n    @overload\n    def setdefault(self: OrderedDict[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ...\n    @overload\n    def setdefault(self, key: _KT, default: _VT) -> _VT: ...\n    # Same as dict.pop, but accepts keyword arguments\n    @overload\n    def pop(self, key: _KT) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _VT) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _T) -> _VT | _T: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    @overload\n    def __or__(self, value: dict[_KT, _VT], /) -> Self: ...\n    @overload\n    def __or__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ...\n    @overload\n    def __ror__(self, value: dict[_KT, _VT], /) -> Self: ...\n    @overload\n    def __ror__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ...  # type: ignore[misc]\n\n@disjoint_base\nclass defaultdict(dict[_KT, _VT]):\n    default_factory: Callable[[], _VT] | None\n    @overload\n    def __init__(self) -> None: ...\n    @overload\n    def __init__(self: defaultdict[str, _VT], **kwargs: _VT) -> None: ...  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n    @overload\n    def __init__(self, default_factory: Callable[[], _VT] | None, /) -> None: ...\n    @overload\n    def __init__(\n        self: defaultdict[str, _VT],  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n        default_factory: Callable[[], _VT] | None,\n        /,\n        **kwargs: _VT,\n    ) -> None: ...\n    @overload\n    def __init__(self, default_factory: Callable[[], _VT] | None, map: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ...\n    @overload\n    def __init__(\n        self: defaultdict[str, _VT],  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n        default_factory: Callable[[], _VT] | None,\n        map: SupportsKeysAndGetItem[str, _VT],\n        /,\n        **kwargs: _VT,\n    ) -> None: ...\n    @overload\n    def __init__(self, default_factory: Callable[[], _VT] | None, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ...\n    @overload\n    def __init__(\n        self: defaultdict[str, _VT],  # pyright: ignore[reportInvalidTypeVarUse]  #11780\n        default_factory: Callable[[], _VT] | None,\n        iterable: Iterable[tuple[str, _VT]],\n        /,\n        **kwargs: _VT,\n    ) -> None: ...\n    def __missing__(self, key: _KT, /) -> _VT: ...\n    def __copy__(self) -> Self: ...\n    def copy(self) -> Self: ...\n    @overload\n    def __or__(self, value: dict[_KT, _VT], /) -> Self: ...\n    @overload\n    def __or__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ...\n    @overload\n    def __ror__(self, value: dict[_KT, _VT], /) -> Self: ...\n    @overload\n    def __ror__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ...  # type: ignore[misc]\n\nclass ChainMap(MutableMapping[_KT, _VT]):\n    maps: list[MutableMapping[_KT, _VT]]\n    def __init__(self, *maps: MutableMapping[_KT, _VT]) -> None: ...\n    def new_child(self, m: MutableMapping[_KT, _VT] | None = None) -> Self: ...\n    @property\n    def parents(self) -> Self: ...\n    def __setitem__(self, key: _KT, value: _VT) -> None: ...\n    def __delitem__(self, key: _KT) -> None: ...\n    def __getitem__(self, key: _KT) -> _VT: ...\n    def __iter__(self) -> Iterator[_KT]: ...\n    def __len__(self) -> int: ...\n    def __contains__(self, key: object) -> bool: ...\n    @overload\n    def get(self, key: _KT, default: None = None) -> _VT | None: ...\n    @overload\n    def get(self, key: _KT, default: _VT) -> _VT: ...\n    @overload\n    def get(self, key: _KT, default: _T) -> _VT | _T: ...\n    def __missing__(self, key: _KT) -> _VT: ...  # undocumented\n    def __bool__(self) -> bool: ...\n    # Keep ChainMap.setdefault in line with MutableMapping.setdefault, modulo positional-only differences.\n    @overload\n    def setdefault(self: ChainMap[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ...\n    @overload\n    def setdefault(self, key: _KT, default: _VT) -> _VT: ...\n    @overload\n    def pop(self, key: _KT) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _VT) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _T) -> _VT | _T: ...\n    def copy(self) -> Self: ...\n    __copy__ = copy\n    # All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime,\n    # so the signature should be kept in line with `dict.fromkeys`.\n    if sys.version_info >= (3, 13):\n        @classmethod\n        @overload\n        def fromkeys(cls, iterable: Iterable[_T], /) -> ChainMap[_T, Any | None]: ...\n    else:\n        @classmethod\n        @overload\n        def fromkeys(cls, iterable: Iterable[_T]) -> ChainMap[_T, Any | None]: ...\n\n    @classmethod\n    @overload\n    # Special-case None: the user probably wants to add non-None values later.\n    def fromkeys(cls, iterable: Iterable[_T], value: None, /) -> ChainMap[_T, Any | None]: ...\n    @classmethod\n    @overload\n    def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> ChainMap[_T, _S]: ...\n    @overload\n    def __or__(self, other: Mapping[_KT, _VT]) -> Self: ...\n    @overload\n    def __or__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ...\n    @overload\n    def __ror__(self, other: Mapping[_KT, _VT]) -> Self: ...\n    @overload\n    def __ror__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ...\n    # ChainMap.__ior__ should be kept roughly in line with MutableMapping.update()\n    @overload  # type: ignore[misc]\n    def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ...\n    @overload\n    def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/collections/abc.pyi",
    "content": "from _collections_abc import *\nfrom _collections_abc import __all__ as __all__\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/dataclasses.pyi",
    "content": "import enum\nimport sys\nimport types\nfrom builtins import type as Type  # alias to avoid name clashes with fields named \"type\"\nfrom collections.abc import Callable, Iterable, Mapping\nfrom types import GenericAlias\nfrom typing import Any, Final, Generic, Literal, Protocol, TypeVar, overload, type_check_only\n\nfrom _typeshed import DataclassInstance\nfrom typing_extensions import Never, TypeIs\n\n_T = TypeVar('_T')\n_T_co = TypeVar('_T_co', covariant=True)\n\n__all__ = [\n    'dataclass',\n    'field',\n    'Field',\n    'FrozenInstanceError',\n    'InitVar',\n    'MISSING',\n    'fields',\n    'asdict',\n    'astuple',\n    'make_dataclass',\n    'replace',\n    'is_dataclass',\n]\n\nif sys.version_info >= (3, 10):\n    __all__ += ['KW_ONLY']\n\n_DataclassT = TypeVar('_DataclassT', bound=DataclassInstance)\n\n@type_check_only\nclass _DataclassFactory(Protocol):\n    def __call__(\n        self,\n        cls: type[_T],\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n        weakref_slot: bool = False,\n    ) -> type[_T]: ...\n\n# define _MISSING_TYPE as an enum within the type stubs,\n# even though that is not really its type at runtime\n# this allows us to use Literal[_MISSING_TYPE.MISSING]\n# for background, see:\n#   https://github.com/python/typeshed/pull/5900#issuecomment-895513797\nclass _MISSING_TYPE(enum.Enum):\n    MISSING = enum.auto()\n\nMISSING: Final = _MISSING_TYPE.MISSING\n\nif sys.version_info >= (3, 10):\n    class KW_ONLY: ...\n\n@overload\ndef asdict(obj: DataclassInstance) -> dict[str, Any]: ...\n@overload\ndef asdict(obj: DataclassInstance, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T: ...\n@overload\ndef astuple(obj: DataclassInstance) -> tuple[Any, ...]: ...\n@overload\ndef astuple(obj: DataclassInstance, *, tuple_factory: Callable[[list[Any]], _T]) -> _T: ...\n\nif sys.version_info >= (3, 11):\n    @overload\n    def dataclass(\n        cls: type[_T],\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n        weakref_slot: bool = False,\n    ) -> type[_T]: ...\n    @overload\n    def dataclass(\n        cls: None = None,\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n        weakref_slot: bool = False,\n    ) -> Callable[[type[_T]], type[_T]]: ...\n\nelif sys.version_info >= (3, 10):\n    @overload\n    def dataclass(\n        cls: type[_T],\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n    ) -> type[_T]: ...\n    @overload\n    def dataclass(\n        cls: None = None,\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n    ) -> Callable[[type[_T]], type[_T]]: ...\n\nelse:\n    @overload\n    def dataclass(\n        cls: type[_T],\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n    ) -> type[_T]: ...\n    @overload\n    def dataclass(\n        cls: None = None,\n        /,\n        *,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n    ) -> Callable[[type[_T]], type[_T]]: ...\n\n# See https://github.com/python/mypy/issues/10750\n@type_check_only\nclass _DefaultFactory(Protocol[_T_co]):\n    def __call__(self) -> _T_co: ...\n\nclass Field(Generic[_T]):\n    if sys.version_info >= (3, 14):\n        __slots__ = (\n            'name',\n            'type',\n            'default',\n            'default_factory',\n            'repr',\n            'hash',\n            'init',\n            'compare',\n            'metadata',\n            'kw_only',\n            'doc',\n            '_field_type',\n        )\n    elif sys.version_info >= (3, 10):\n        __slots__ = (\n            'name',\n            'type',\n            'default',\n            'default_factory',\n            'repr',\n            'hash',\n            'init',\n            'compare',\n            'metadata',\n            'kw_only',\n            '_field_type',\n        )\n    else:\n        __slots__ = (\n            'name',\n            'type',\n            'default',\n            'default_factory',\n            'repr',\n            'hash',\n            'init',\n            'compare',\n            'metadata',\n            '_field_type',\n        )\n    name: str\n    type: Type[_T] | str | Any\n    default: _T | Literal[_MISSING_TYPE.MISSING]\n    default_factory: _DefaultFactory[_T] | Literal[_MISSING_TYPE.MISSING]\n    repr: bool\n    hash: bool | None\n    init: bool\n    compare: bool\n    metadata: types.MappingProxyType[Any, Any]\n\n    if sys.version_info >= (3, 14):\n        doc: str | None\n\n    if sys.version_info >= (3, 10):\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING]\n\n    if sys.version_info >= (3, 14):\n        def __init__(\n            self,\n            default: _T,\n            default_factory: Callable[[], _T],\n            init: bool,\n            repr: bool,\n            hash: bool | None,\n            compare: bool,\n            metadata: Mapping[Any, Any],\n            kw_only: bool,\n            doc: str | None,\n        ) -> None: ...\n    elif sys.version_info >= (3, 10):\n        def __init__(\n            self,\n            default: _T,\n            default_factory: Callable[[], _T],\n            init: bool,\n            repr: bool,\n            hash: bool | None,\n            compare: bool,\n            metadata: Mapping[Any, Any],\n            kw_only: bool,\n        ) -> None: ...\n    else:\n        def __init__(\n            self,\n            default: _T,\n            default_factory: Callable[[], _T],\n            init: bool,\n            repr: bool,\n            hash: bool | None,\n            compare: bool,\n            metadata: Mapping[Any, Any],\n        ) -> None: ...\n\n    def __set_name__(self, owner: Type[Any], name: str) -> None: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n# NOTE: Actual return type is 'Field[_T]', but we want to help type checkers\n# to understand the magic that happens at runtime.\nif sys.version_info >= (3, 14):\n    @overload  # `default` and `default_factory` are optional and mutually exclusive.\n    def field(\n        *,\n        default: _T,\n        default_factory: Literal[_MISSING_TYPE.MISSING] = ...,\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ...,\n        doc: str | None = None,\n    ) -> _T: ...\n    @overload\n    def field(\n        *,\n        default: Literal[_MISSING_TYPE.MISSING] = ...,\n        default_factory: Callable[[], _T],\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ...,\n        doc: str | None = None,\n    ) -> _T: ...\n    @overload\n    def field(\n        *,\n        default: Literal[_MISSING_TYPE.MISSING] = ...,\n        default_factory: Literal[_MISSING_TYPE.MISSING] = ...,\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ...,\n        doc: str | None = None,\n    ) -> Any: ...\n\nelif sys.version_info >= (3, 10):\n    @overload  # `default` and `default_factory` are optional and mutually exclusive.\n    def field(\n        *,\n        default: _T,\n        default_factory: Literal[_MISSING_TYPE.MISSING] = ...,\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ...,\n    ) -> _T: ...\n    @overload\n    def field(\n        *,\n        default: Literal[_MISSING_TYPE.MISSING] = ...,\n        default_factory: Callable[[], _T],\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ...,\n    ) -> _T: ...\n    @overload\n    def field(\n        *,\n        default: Literal[_MISSING_TYPE.MISSING] = ...,\n        default_factory: Literal[_MISSING_TYPE.MISSING] = ...,\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n        kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ...,\n    ) -> Any: ...\n\nelse:\n    @overload  # `default` and `default_factory` are optional and mutually exclusive.\n    def field(\n        *,\n        default: _T,\n        default_factory: Literal[_MISSING_TYPE.MISSING] = ...,\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n    ) -> _T: ...\n    @overload\n    def field(\n        *,\n        default: Literal[_MISSING_TYPE.MISSING] = ...,\n        default_factory: Callable[[], _T],\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n    ) -> _T: ...\n    @overload\n    def field(\n        *,\n        default: Literal[_MISSING_TYPE.MISSING] = ...,\n        default_factory: Literal[_MISSING_TYPE.MISSING] = ...,\n        init: bool = True,\n        repr: bool = True,\n        hash: bool | None = None,\n        compare: bool = True,\n        metadata: Mapping[Any, Any] | None = None,\n    ) -> Any: ...\n\ndef fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tuple[Field[Any], ...]: ...\n\n# HACK: `obj: Never` typing matches if object argument is using `Any` type.\n@overload\ndef is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ...  # type: ignore[narrowed-type-not-subtype]  # pyright: ignore[reportGeneralTypeIssues]\n@overload\ndef is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ...\n@overload\ndef is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ...\n\nclass FrozenInstanceError(AttributeError): ...\n\nclass InitVar(Generic[_T]):\n    __slots__ = ('type',)\n    type: Type[_T]\n    def __init__(self, type: Type[_T]) -> None: ...\n    @overload\n    def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ...  # pyright: ignore[reportInvalidTypeForm]\n    @overload\n    def __class_getitem__(cls, type: Any) -> InitVar[Any]: ...  # pyright: ignore[reportInvalidTypeForm]\n\nif sys.version_info >= (3, 14):\n    def make_dataclass(\n        cls_name: str,\n        fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]],\n        *,\n        bases: tuple[type, ...] = (),\n        namespace: dict[str, Any] | None = None,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n        weakref_slot: bool = False,\n        module: str | None = None,\n        decorator: _DataclassFactory = ...,\n    ) -> type: ...\n\nelif sys.version_info >= (3, 12):\n    def make_dataclass(\n        cls_name: str,\n        fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]],\n        *,\n        bases: tuple[type, ...] = (),\n        namespace: dict[str, Any] | None = None,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n        weakref_slot: bool = False,\n        module: str | None = None,\n    ) -> type: ...\n\nelif sys.version_info >= (3, 11):\n    def make_dataclass(\n        cls_name: str,\n        fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]],\n        *,\n        bases: tuple[type, ...] = (),\n        namespace: dict[str, Any] | None = None,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n        weakref_slot: bool = False,\n    ) -> type: ...\n\nelif sys.version_info >= (3, 10):\n    def make_dataclass(\n        cls_name: str,\n        fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]],\n        *,\n        bases: tuple[type, ...] = (),\n        namespace: dict[str, Any] | None = None,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n        match_args: bool = True,\n        kw_only: bool = False,\n        slots: bool = False,\n    ) -> type: ...\n\nelse:\n    def make_dataclass(\n        cls_name: str,\n        fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]],\n        *,\n        bases: tuple[type, ...] = (),\n        namespace: dict[str, Any] | None = None,\n        init: bool = True,\n        repr: bool = True,\n        eq: bool = True,\n        order: bool = False,\n        unsafe_hash: bool = False,\n        frozen: bool = False,\n    ) -> type: ...\n\ndef replace(obj: _DataclassT, /, **changes: Any) -> _DataclassT: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/enum.pyi",
    "content": "import sys\nimport types\nfrom builtins import property as _builtins_property\nfrom collections.abc import Callable, Iterable, Iterator, Mapping\nfrom typing import Any, Final, Generic, Literal, SupportsIndex, TypeVar, overload\n\nimport _typeshed\nfrom _typeshed import SupportsKeysAndGetItem, Unused\nfrom typing_extensions import Self, TypeAlias, disjoint_base\n\n__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flag', 'IntFlag', 'auto', 'unique']\n\nif sys.version_info >= (3, 11):\n    __all__ += [\n        'CONFORM',\n        'CONTINUOUS',\n        'EJECT',\n        'EnumCheck',\n        'EnumType',\n        'FlagBoundary',\n        'KEEP',\n        'NAMED_FLAGS',\n        'ReprEnum',\n        'STRICT',\n        'StrEnum',\n        'UNIQUE',\n        'global_enum',\n        'global_enum_repr',\n        'global_flag_repr',\n        'global_str',\n        'member',\n        'nonmember',\n        'property',\n        'verify',\n        'pickle_by_enum_name',\n        'pickle_by_global_name',\n    ]\n\nif sys.version_info >= (3, 13):\n    __all__ += ['EnumDict']\n\n_EnumMemberT = TypeVar('_EnumMemberT')\n_EnumerationT = TypeVar('_EnumerationT', bound=type[Enum])\n\n# The following all work:\n# >>> from enum import Enum\n# >>> from string import ascii_lowercase\n# >>> Enum('Foo', names='RED YELLOW GREEN')\n# <enum 'Foo'>\n# >>> Enum('Foo', names=[('RED', 1), ('YELLOW, 2)])\n# <enum 'Foo'>\n# >>> Enum('Foo', names=((x for x in (ascii_lowercase[i], i)) for i in range(5)))\n# <enum 'Foo'>\n# >>> Enum('Foo', names={'RED': 1, 'YELLOW': 2})\n# <enum 'Foo'>\n_EnumNames: TypeAlias = str | Iterable[str] | Iterable[Iterable[str | Any]] | Mapping[str, Any]\n_Signature: TypeAlias = Any  # TODO: Unable to import Signature from inspect module\n\nif sys.version_info >= (3, 11):\n    class nonmember(Generic[_EnumMemberT]):\n        value: _EnumMemberT\n        def __init__(self, value: _EnumMemberT) -> None: ...\n\n    class member(Generic[_EnumMemberT]):\n        value: _EnumMemberT\n        def __init__(self, value: _EnumMemberT) -> None: ...\n\nclass _EnumDict(dict[str, Any]):\n    if sys.version_info >= (3, 13):\n        def __init__(self, cls_name: str | None = None) -> None: ...\n    else:\n        def __init__(self) -> None: ...\n\n    def __setitem__(self, key: str, value: Any) -> None: ...\n    if sys.version_info >= (3, 11):\n        # See comment above `typing.MutableMapping.update`\n        # for why overloads are preferable to a Union here\n        #\n        # Unlike with MutableMapping.update(), the first argument is required,\n        # hence the type: ignore\n        @overload  # type: ignore[override]\n        def update(self, members: SupportsKeysAndGetItem[str, Any], **more_members: Any) -> None: ...\n        @overload\n        def update(self, members: Iterable[tuple[str, Any]], **more_members: Any) -> None: ...\n    if sys.version_info >= (3, 13):\n        @property\n        def member_names(self) -> list[str]: ...\n\nif sys.version_info >= (3, 13):\n    EnumDict = _EnumDict\n\n# Structurally: Iterable[T], Reversible[T], Container[T] where T is the enum itself\nclass EnumMeta(type):\n    if sys.version_info >= (3, 11):\n        def __new__(\n            metacls: type[_typeshed.Self],\n            cls: str,\n            bases: tuple[type, ...],\n            classdict: _EnumDict,\n            *,\n            boundary: FlagBoundary | None = None,\n            _simple: bool = False,\n            **kwds: Any,\n        ) -> _typeshed.Self: ...\n    else:\n        def __new__(\n            metacls: type[_typeshed.Self], cls: str, bases: tuple[type, ...], classdict: _EnumDict, **kwds: Any\n        ) -> _typeshed.Self: ...\n\n    @classmethod\n    def __prepare__(metacls, cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict: ...  # type: ignore[override]\n    def __iter__(self: type[_EnumMemberT]) -> Iterator[_EnumMemberT]: ...\n    def __reversed__(self: type[_EnumMemberT]) -> Iterator[_EnumMemberT]: ...\n    if sys.version_info >= (3, 12):\n        def __contains__(self: type[Any], value: object) -> bool: ...\n    elif sys.version_info >= (3, 11):\n        def __contains__(self: type[Any], member: object) -> bool: ...\n    elif sys.version_info >= (3, 10):\n        def __contains__(self: type[Any], obj: object) -> bool: ...\n    else:\n        def __contains__(self: type[Any], member: object) -> bool: ...\n\n    def __getitem__(self: type[_EnumMemberT], name: str) -> _EnumMemberT: ...\n    @_builtins_property\n    def __members__(self: type[_EnumMemberT]) -> types.MappingProxyType[str, _EnumMemberT]: ...\n    def __len__(self) -> int: ...\n    def __bool__(self) -> Literal[True]: ...\n    def __dir__(self) -> list[str]: ...\n\n    # Overload 1: Value lookup on an already existing enum class (simple case)\n    @overload\n    def __call__(cls: type[_EnumMemberT], value: Any, names: None = None) -> _EnumMemberT: ...\n\n    # Overload 2: Functional API for constructing new enum classes.\n    if sys.version_info >= (3, 11):\n        @overload\n        def __call__(\n            cls,\n            value: str,\n            names: _EnumNames,\n            *,\n            module: str | None = None,\n            qualname: str | None = None,\n            type: type | None = None,\n            start: int = 1,\n            boundary: FlagBoundary | None = None,\n        ) -> type[Enum]: ...\n    else:\n        @overload\n        def __call__(\n            cls,\n            value: str,\n            names: _EnumNames,\n            *,\n            module: str | None = None,\n            qualname: str | None = None,\n            type: type | None = None,\n            start: int = 1,\n        ) -> type[Enum]: ...\n\n    # Overload 3 (py312+ only): Value lookup on an already existing enum class (complex case)\n    #\n    # >>> class Foo(enum.Enum):\n    # ...     X = 1, 2, 3\n    # >>> Foo(1, 2, 3)\n    # <Foo.X: (1, 2, 3)>\n    #\n    if sys.version_info >= (3, 12):\n        @overload\n        def __call__(cls: type[_EnumMemberT], value: Any, *values: Any) -> _EnumMemberT: ...\n    if sys.version_info >= (3, 14):\n        @property\n        def __signature__(cls) -> _Signature: ...\n\n    _member_names_: list[str]  # undocumented\n    _member_map_: dict[str, Enum]  # undocumented\n    _value2member_map_: dict[Any, Enum]  # undocumented\n\nif sys.version_info >= (3, 11):\n    # In 3.11 `EnumMeta` metaclass is renamed to `EnumType`, but old name also exists.\n    EnumType = EnumMeta\n\n    class property(types.DynamicClassAttribute):\n        def __set_name__(self, ownerclass: type[Enum], name: str) -> None: ...\n        name: str\n        clsname: str\n        member: Enum | None\n\n    _magic_enum_attr = property\nelse:\n    _magic_enum_attr = types.DynamicClassAttribute\n\nclass Enum(metaclass=EnumMeta):\n    @_magic_enum_attr\n    def name(self) -> str: ...\n    @_magic_enum_attr\n    def value(self) -> Any: ...\n    _name_: str\n    _value_: Any\n    _ignore_: str | list[str]\n    _order_: str\n    __order__: str\n    @classmethod\n    def _missing_(cls, value: object) -> Any: ...\n    @staticmethod\n    def _generate_next_value_(name: str, start: int, count: int, last_values: list[Any]) -> Any: ...\n    # It's not true that `__new__` will accept any argument type,\n    # so ideally we'd use `Any` to indicate that the argument type is inexpressible.\n    # However, using `Any` causes too many false-positives for those using mypy's `--disallow-any-expr`\n    # (see #7752, #2539, mypy/#5788),\n    # and in practice using `object` here has the same effect as using `Any`.\n    def __new__(cls, value: object) -> Self: ...\n    def __dir__(self) -> list[str]: ...\n    def __hash__(self) -> int: ...\n    def __format__(self, format_spec: str) -> str: ...\n    def __reduce_ex__(self, proto: Unused) -> tuple[Any, ...]: ...\n    if sys.version_info >= (3, 11):\n        def __copy__(self) -> Self: ...\n        def __deepcopy__(self, memo: Any) -> Self: ...\n    if sys.version_info >= (3, 12) and sys.version_info < (3, 14):\n        @classmethod\n        def __signature__(cls) -> str: ...\n    if sys.version_info >= (3, 13):\n        # Value may be any type, even in special enums. Enabling Enum parsing from\n        # multiple value types\n        def _add_value_alias_(self, value: Any) -> None: ...\n        def _add_alias_(self, name: str) -> None: ...\n\nif sys.version_info >= (3, 11):\n    class ReprEnum(Enum): ...\n\nif sys.version_info >= (3, 12):\n    class IntEnum(int, ReprEnum):\n        _value_: int\n        @_magic_enum_attr\n        def value(self) -> int: ...\n        def __new__(cls, value: int) -> Self: ...\n\nelse:\n    if sys.version_info >= (3, 11):\n        _IntEnumBase = ReprEnum\n    else:\n        _IntEnumBase = Enum\n\n    @disjoint_base\n    class IntEnum(int, _IntEnumBase):\n        _value_: int\n        @_magic_enum_attr\n        def value(self) -> int: ...\n        def __new__(cls, value: int) -> Self: ...\n\ndef unique(enumeration: _EnumerationT) -> _EnumerationT: ...\n\n_auto_null: Any\n\nclass Flag(Enum):\n    _name_: str | None  # type: ignore[assignment]\n    _value_: int\n    _numeric_repr_: Callable[[int], str]\n    @_magic_enum_attr\n    def name(self) -> str | None: ...  # type: ignore[override]\n    @_magic_enum_attr\n    def value(self) -> int: ...\n    def __contains__(self, other: Self) -> bool: ...\n    def __bool__(self) -> bool: ...\n    def __or__(self, other: Self) -> Self: ...\n    def __and__(self, other: Self) -> Self: ...\n    def __xor__(self, other: Self) -> Self: ...\n    def __invert__(self) -> Self: ...\n    if sys.version_info >= (3, 11):\n        def __iter__(self) -> Iterator[Self]: ...\n        def __len__(self) -> int: ...\n        __ror__ = __or__\n        __rand__ = __and__\n        __rxor__ = __xor__\n\nif sys.version_info >= (3, 11):\n    class StrEnum(str, ReprEnum):\n        def __new__(cls, value: str) -> Self: ...\n        _value_: str\n        @_magic_enum_attr\n        def value(self) -> str: ...\n        @staticmethod\n        def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str: ...\n\n    class EnumCheck(StrEnum):\n        CONTINUOUS = 'no skipped integer values'\n        NAMED_FLAGS = 'multi-flag aliases may not contain unnamed flags'\n        UNIQUE = 'one name per value'\n\n    CONTINUOUS: Final = EnumCheck.CONTINUOUS\n    NAMED_FLAGS: Final = EnumCheck.NAMED_FLAGS\n    UNIQUE: Final = EnumCheck.UNIQUE\n\n    class verify:\n        def __init__(self, *checks: EnumCheck) -> None: ...\n        def __call__(self, enumeration: _EnumerationT) -> _EnumerationT: ...\n\n    class FlagBoundary(StrEnum):\n        STRICT = 'strict'\n        CONFORM = 'conform'\n        EJECT = 'eject'\n        KEEP = 'keep'\n\n    STRICT: Final = FlagBoundary.STRICT\n    CONFORM: Final = FlagBoundary.CONFORM\n    EJECT: Final = FlagBoundary.EJECT\n    KEEP: Final = FlagBoundary.KEEP\n\n    def global_str(self: Enum) -> str: ...\n    def global_enum(cls: _EnumerationT, update_str: bool = False) -> _EnumerationT: ...\n    def global_enum_repr(self: Enum) -> str: ...\n    def global_flag_repr(self: Flag) -> str: ...\n    def show_flag_values(value: int) -> list[int]: ...\n    def bin(num: SupportsIndex, max_bits: int | None = None) -> str: ...\n\nif sys.version_info >= (3, 12):\n    # The body of the class is the same, but the base classes are different.\n    class IntFlag(int, ReprEnum, Flag, boundary=KEEP):  # type: ignore[misc]  # complaints about incompatible bases\n        def __new__(cls, value: int) -> Self: ...\n        def __or__(self, other: int) -> Self: ...\n        def __and__(self, other: int) -> Self: ...\n        def __xor__(self, other: int) -> Self: ...\n        def __invert__(self) -> Self: ...\n        __ror__ = __or__\n        __rand__ = __and__\n        __rxor__ = __xor__\n\nelif sys.version_info >= (3, 11):\n    # The body of the class is the same, but the base classes are different.\n    @disjoint_base\n    class IntFlag(int, ReprEnum, Flag, boundary=KEEP):  # type: ignore[misc]  # complaints about incompatible bases\n        def __new__(cls, value: int) -> Self: ...\n        def __or__(self, other: int) -> Self: ...\n        def __and__(self, other: int) -> Self: ...\n        def __xor__(self, other: int) -> Self: ...\n        def __invert__(self) -> Self: ...\n        __ror__ = __or__\n        __rand__ = __and__\n        __rxor__ = __xor__\n\nelse:\n    @disjoint_base\n    class IntFlag(int, Flag):  # type: ignore[misc]  # complaints about incompatible bases\n        def __new__(cls, value: int) -> Self: ...\n        def __or__(self, other: int) -> Self: ...\n        def __and__(self, other: int) -> Self: ...\n        def __xor__(self, other: int) -> Self: ...\n        def __invert__(self) -> Self: ...\n        __ror__ = __or__\n        __rand__ = __and__\n        __rxor__ = __xor__\n\nclass auto:\n    _value_: Any\n    @_magic_enum_attr\n    def value(self) -> Any: ...\n    def __new__(cls) -> Self: ...\n\n    # These don't exist, but auto is basically immediately replaced with\n    # either an int or a str depending on the type of the enum. StrEnum's auto\n    # shouldn't have these, but they're needed for int versions of auto (mostly the __or__).\n    # Ideally type checkers would special case auto enough to handle this,\n    # but until then this is a slightly inaccurate helping hand.\n    def __or__(self, other: int | Self) -> Self: ...\n    def __and__(self, other: int | Self) -> Self: ...\n    def __xor__(self, other: int | Self) -> Self: ...\n    __ror__ = __or__\n    __rand__ = __and__\n    __rxor__ = __xor__\n\nif sys.version_info >= (3, 11):\n    def pickle_by_global_name(self: Enum, proto: int) -> str: ...\n    def pickle_by_enum_name(\n        self: _EnumMemberT, proto: int\n    ) -> tuple[Callable[..., Any], tuple[type[_EnumMemberT], str]]: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/math.pyi",
    "content": "import sys\nfrom collections.abc import Iterable\nfrom typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypeVar, overload, type_check_only\n\nfrom _typeshed import SupportsMul, SupportsRMul\nfrom typing_extensions import TypeAlias\n\n_T = TypeVar('_T')\n_T_co = TypeVar('_T_co', covariant=True)\n\n_SupportsFloatOrIndex: TypeAlias = SupportsFloat | SupportsIndex\n\ne: Final[float]\npi: Final[float]\ninf: Final[float]\nnan: Final[float]\ntau: Final[float]\n\ndef acos(x: _SupportsFloatOrIndex, /) -> float: ...\ndef acosh(x: _SupportsFloatOrIndex, /) -> float: ...\ndef asin(x: _SupportsFloatOrIndex, /) -> float: ...\ndef asinh(x: _SupportsFloatOrIndex, /) -> float: ...\ndef atan(x: _SupportsFloatOrIndex, /) -> float: ...\ndef atan2(y: _SupportsFloatOrIndex, x: _SupportsFloatOrIndex, /) -> float: ...\ndef atanh(x: _SupportsFloatOrIndex, /) -> float: ...\n\nif sys.version_info >= (3, 11):\n    def cbrt(x: _SupportsFloatOrIndex, /) -> float: ...\n\n@type_check_only\nclass _SupportsCeil(Protocol[_T_co]):\n    def __ceil__(self) -> _T_co: ...\n\n@overload\ndef ceil(x: _SupportsCeil[_T], /) -> _T: ...\n@overload\ndef ceil(x: _SupportsFloatOrIndex, /) -> int: ...\ndef comb(n: SupportsIndex, k: SupportsIndex, /) -> int: ...\ndef copysign(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ...\ndef cos(x: _SupportsFloatOrIndex, /) -> float: ...\ndef cosh(x: _SupportsFloatOrIndex, /) -> float: ...\ndef degrees(x: _SupportsFloatOrIndex, /) -> float: ...\ndef dist(p: Iterable[_SupportsFloatOrIndex], q: Iterable[_SupportsFloatOrIndex], /) -> float: ...\ndef erf(x: _SupportsFloatOrIndex, /) -> float: ...\ndef erfc(x: _SupportsFloatOrIndex, /) -> float: ...\ndef exp(x: _SupportsFloatOrIndex, /) -> float: ...\n\nif sys.version_info >= (3, 11):\n    def exp2(x: _SupportsFloatOrIndex, /) -> float: ...\n\ndef expm1(x: _SupportsFloatOrIndex, /) -> float: ...\ndef fabs(x: _SupportsFloatOrIndex, /) -> float: ...\ndef factorial(x: SupportsIndex, /) -> int: ...\n@type_check_only\nclass _SupportsFloor(Protocol[_T_co]):\n    def __floor__(self) -> _T_co: ...\n\n@overload\ndef floor(x: _SupportsFloor[_T], /) -> _T: ...\n@overload\ndef floor(x: _SupportsFloatOrIndex, /) -> int: ...\ndef fmod(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ...\ndef frexp(x: _SupportsFloatOrIndex, /) -> tuple[float, int]: ...\ndef fsum(seq: Iterable[_SupportsFloatOrIndex], /) -> float: ...\ndef gamma(x: _SupportsFloatOrIndex, /) -> float: ...\ndef gcd(*integers: SupportsIndex) -> int: ...\ndef hypot(*coordinates: _SupportsFloatOrIndex) -> float: ...\ndef isclose(\n    a: _SupportsFloatOrIndex,\n    b: _SupportsFloatOrIndex,\n    *,\n    rel_tol: _SupportsFloatOrIndex = 1e-09,\n    abs_tol: _SupportsFloatOrIndex = 0.0,\n) -> bool: ...\ndef isinf(x: _SupportsFloatOrIndex, /) -> bool: ...\ndef isfinite(x: _SupportsFloatOrIndex, /) -> bool: ...\ndef isnan(x: _SupportsFloatOrIndex, /) -> bool: ...\ndef isqrt(n: SupportsIndex, /) -> int: ...\ndef lcm(*integers: SupportsIndex) -> int: ...\ndef ldexp(x: _SupportsFloatOrIndex, i: int, /) -> float: ...\ndef lgamma(x: _SupportsFloatOrIndex, /) -> float: ...\ndef log(x: _SupportsFloatOrIndex, base: _SupportsFloatOrIndex = ...) -> float: ...\ndef log10(x: _SupportsFloatOrIndex, /) -> float: ...\ndef log1p(x: _SupportsFloatOrIndex, /) -> float: ...\ndef log2(x: _SupportsFloatOrIndex, /) -> float: ...\ndef modf(x: _SupportsFloatOrIndex, /) -> tuple[float, float]: ...\n\nif sys.version_info >= (3, 12):\n    def nextafter(\n        x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /, *, steps: SupportsIndex | None = None\n    ) -> float: ...\n\nelse:\n    def nextafter(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ...\n\ndef perm(n: SupportsIndex, k: SupportsIndex | None = None, /) -> int: ...\ndef pow(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ...\n\n_PositiveInteger: TypeAlias = Literal[\n    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25\n]\n_NegativeInteger: TypeAlias = Literal[\n    -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20\n]\n_LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0]  # TODO: Use TypeAlias once mypy bugs are fixed\n\n_MultiplicableT1 = TypeVar('_MultiplicableT1', bound=SupportsMul[Any, Any])\n_MultiplicableT2 = TypeVar('_MultiplicableT2', bound=SupportsMul[Any, Any])\n\n@type_check_only\nclass _SupportsProdWithNoDefaultGiven(SupportsMul[Any, Any], SupportsRMul[int, Any], Protocol): ...\n\n_SupportsProdNoDefaultT = TypeVar('_SupportsProdNoDefaultT', bound=_SupportsProdWithNoDefaultGiven)\n\n# This stub is based on the type stub for `builtins.sum`.\n# Like `builtins.sum`, it cannot be precisely represented in a type stub\n# without introducing many false positives.\n# For more details on its limitations and false positives, see #13572.\n# Instead, just like `builtins.sum`, we explicitly handle several useful cases.\n@overload\ndef prod(iterable: Iterable[bool | _LiteralInteger], /, *, start: int = 1) -> int: ...  # type: ignore[overload-overlap]\n@overload\ndef prod(iterable: Iterable[_SupportsProdNoDefaultT], /) -> _SupportsProdNoDefaultT | Literal[1]: ...\n@overload\ndef prod(\n    iterable: Iterable[_MultiplicableT1], /, *, start: _MultiplicableT2\n) -> _MultiplicableT1 | _MultiplicableT2: ...\ndef radians(x: _SupportsFloatOrIndex, /) -> float: ...\ndef remainder(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ...\ndef sin(x: _SupportsFloatOrIndex, /) -> float: ...\ndef sinh(x: _SupportsFloatOrIndex, /) -> float: ...\n\nif sys.version_info >= (3, 12):\n    def sumprod(p: Iterable[float], q: Iterable[float], /) -> float: ...\n\ndef sqrt(x: _SupportsFloatOrIndex, /) -> float: ...\ndef tan(x: _SupportsFloatOrIndex, /) -> float: ...\ndef tanh(x: _SupportsFloatOrIndex, /) -> float: ...\n\n# Is different from `_typeshed.SupportsTrunc`, which is not generic\n@type_check_only\nclass _SupportsTrunc(Protocol[_T_co]):\n    def __trunc__(self) -> _T_co: ...\n\ndef trunc(x: _SupportsTrunc[_T], /) -> _T: ...\ndef ulp(x: _SupportsFloatOrIndex, /) -> float: ...\n\nif sys.version_info >= (3, 13):\n    def fma(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, z: _SupportsFloatOrIndex, /) -> float: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/os.pyi",
    "content": "from abc import ABC, abstractmethod\nfrom typing import Callable, Protocol, TypeAlias, TypeVar, final, overload, runtime_checkable\n\nfrom _typeshed import AnyStr_co, structseq\n\n_T = TypeVar('_T')\nenviron: dict[str, str]\n\n@overload\ndef getenv(key: str) -> str | None: ...\n@overload\ndef getenv(key: str, default: _T) -> str | _T: ...\n@final\nclass stat_result(structseq[float], tuple[int, int, int, int, int, int, int, float, float, float]):\n    # The constructor of this class takes an iterable of variable length (though it must be at least 10).\n    #\n    # However, this class behaves like a tuple of 10 elements,\n    # no matter how long the iterable supplied to the constructor is.\n    # https://github.com/python/typeshed/pull/6560#discussion_r767162532\n    #\n    # The 10 elements always present are st_mode, st_ino, st_dev, st_nlink,\n    # st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime.\n    #\n    # More items may be added at the end by some implementations.\n\n    @property\n    def st_mode(self) -> int:\n        \"\"\"protection bits\"\"\"\n        ...\n\n    @property\n    def st_ino(self) -> int:\n        \"\"\"inode\"\"\"\n        ...\n\n    @property\n    def st_dev(self) -> int:\n        \"\"\"device\"\"\"\n        ...\n\n    @property\n    def st_nlink(self) -> int:\n        \"\"\"number of hard links\"\"\"\n        ...\n\n    @property\n    def st_uid(self) -> int:\n        \"\"\"user ID of owner\"\"\"\n        ...\n\n    @property\n    def st_gid(self) -> int:\n        \"\"\"group ID of owner\"\"\"\n        ...\n\n    @property\n    def st_size(self) -> int:\n        \"\"\"total size, in bytes\"\"\"\n        ...\n\n    @property\n    def st_atime(self) -> float:\n        \"\"\"time of last access\"\"\"\n        ...\n\n    @property\n    def st_mtime(self) -> float:\n        \"\"\"time of last modification\"\"\"\n        ...\n\n    @property\n    def st_ctime(self) -> float:\n        \"\"\"time of last change\"\"\"\n        ...\n\n# (Samuel) PathLike is included here because it's used by pathlib\n\n# mypy and pyright object to this being both ABC and Protocol.\n# At runtime it inherits from ABC and is not a Protocol, but it will be\n# on the allowlist for use as a Protocol starting in 3.14.\n@runtime_checkable\nclass PathLike(ABC, Protocol[AnyStr_co]):  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n    __slots__ = ()\n    @abstractmethod\n    def __fspath__(self) -> AnyStr_co: ...\n\n_Opener: TypeAlias = Callable[[str, int], int]\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/pathlib/__init__.pyi",
    "content": "import sys\nimport types\nfrom collections.abc import Callable, Generator, Iterator, Sequence\nfrom io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper\nfrom os import PathLike, stat_result\nfrom types import GenericAlias, TracebackType\nfrom typing import IO, Any, BinaryIO, ClassVar, Literal, TypeVar, overload\n\nfrom _typeshed import (\n    OpenBinaryMode,\n    OpenBinaryModeReading,\n    OpenBinaryModeUpdating,\n    OpenBinaryModeWriting,\n    OpenTextMode,\n    ReadableBuffer,\n    StrOrBytesPath,\n    StrPath,\n    Unused,\n)\nfrom typing_extensions import Never, Self, deprecated\n\n_PathT = TypeVar('_PathT', bound=PurePath)\n\n__all__ = ['PurePath', 'PurePosixPath', 'PureWindowsPath', 'Path', 'PosixPath', 'WindowsPath']\n\nif sys.version_info >= (3, 14):\n    from pathlib.types import PathInfo\n\nif sys.version_info >= (3, 13):\n    __all__ += ['UnsupportedOperation']\n\nclass PurePath(PathLike[str]):\n    if sys.version_info >= (3, 13):\n        __slots__ = (\n            '_raw_paths',\n            '_drv',\n            '_root',\n            '_tail_cached',\n            '_str',\n            '_str_normcase_cached',\n            '_parts_normcase_cached',\n            '_hash',\n        )\n    elif sys.version_info >= (3, 12):\n        __slots__ = (\n            '_raw_paths',\n            '_drv',\n            '_root',\n            '_tail_cached',\n            '_str',\n            '_str_normcase_cached',\n            '_parts_normcase_cached',\n            '_lines_cached',\n            '_hash',\n        )\n    else:\n        __slots__ = ('_drv', '_root', '_parts', '_str', '_hash', '_pparts', '_cached_cparts')\n    if sys.version_info >= (3, 13):\n        parser: ClassVar[types.ModuleType]\n        def full_match(self, pattern: StrPath, *, case_sensitive: bool | None = None) -> bool: ...\n\n    @property\n    def parts(self) -> tuple[str, ...]: ...\n    @property\n    def drive(self) -> str: ...\n    @property\n    def root(self) -> str: ...\n    @property\n    def anchor(self) -> str: ...\n    @property\n    def name(self) -> str: ...\n    @property\n    def suffix(self) -> str: ...\n    @property\n    def suffixes(self) -> list[str]: ...\n    @property\n    def stem(self) -> str: ...\n    if sys.version_info >= (3, 12):\n        def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ...\n        def __init__(self, *args: StrPath) -> None: ...  # pyright: ignore[reportInconsistentConstructor]\n    else:\n        def __new__(cls, *args: StrPath) -> Self: ...\n\n    def __hash__(self) -> int: ...\n    def __fspath__(self) -> str: ...\n    def __lt__(self, other: PurePath) -> bool: ...\n    def __le__(self, other: PurePath) -> bool: ...\n    def __gt__(self, other: PurePath) -> bool: ...\n    def __ge__(self, other: PurePath) -> bool: ...\n    def __truediv__(self, key: StrPath) -> Self: ...\n    def __rtruediv__(self, key: StrPath) -> Self: ...\n    def __bytes__(self) -> bytes: ...\n    def as_posix(self) -> str: ...\n    @deprecated('Deprecated since Python 3.14; will be removed in Python 3.19. Use `Path.as_uri()` instead.')\n    def as_uri(self) -> str: ...\n    def is_absolute(self) -> bool: ...\n    if sys.version_info >= (3, 13):\n        @deprecated(\n            'Deprecated since Python 3.13; will be removed in Python 3.15. '\n            'Use `os.path.isreserved()` to detect reserved paths on Windows.'\n        )\n        def is_reserved(self) -> bool: ...\n    else:\n        def is_reserved(self) -> bool: ...\n    if sys.version_info >= (3, 14):\n        def is_relative_to(self, other: StrPath) -> bool: ...\n    elif sys.version_info >= (3, 12):\n        @overload\n        def is_relative_to(self, other: StrPath, /) -> bool: ...\n        @overload\n        @deprecated('Passing additional arguments is deprecated since Python 3.12; removed in Python 3.14.')\n        def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool: ...\n    else:\n        def is_relative_to(self, *other: StrPath) -> bool: ...\n\n    if sys.version_info >= (3, 12):\n        def match(self, path_pattern: str, *, case_sensitive: bool | None = None) -> bool: ...\n    else:\n        def match(self, path_pattern: str) -> bool: ...\n\n    if sys.version_info >= (3, 14):\n        def relative_to(self, other: StrPath, *, walk_up: bool = False) -> Self: ...\n    elif sys.version_info >= (3, 12):\n        @overload\n        def relative_to(self, other: StrPath, /, *, walk_up: bool = False) -> Self: ...\n        @overload\n        @deprecated('Passing additional arguments is deprecated since Python 3.12; removed in Python 3.14.')\n        def relative_to(self, other: StrPath, /, *_deprecated: StrPath, walk_up: bool = False) -> Self: ...\n    else:\n        def relative_to(self, *other: StrPath) -> Self: ...\n\n    def with_name(self, name: str) -> Self: ...\n    def with_stem(self, stem: str) -> Self: ...\n    def with_suffix(self, suffix: str) -> Self: ...\n    def joinpath(self, *other: StrPath) -> Self: ...\n    @property\n    def parents(self) -> Sequence[Self]: ...\n    @property\n    def parent(self) -> Self: ...\n    if sys.version_info < (3, 11):\n        def __class_getitem__(cls, type: Any) -> GenericAlias: ...\n\n    if sys.version_info >= (3, 12):\n        def with_segments(self, *args: StrPath) -> Self: ...\n\nclass PurePosixPath(PurePath):\n    __slots__ = ()\n\nclass PureWindowsPath(PurePath):\n    __slots__ = ()\n\nclass Path(PurePath):\n    if sys.version_info >= (3, 14):\n        __slots__ = ('_info',)\n    elif sys.version_info >= (3, 10):\n        __slots__ = ()\n    else:\n        __slots__ = ('_accessor',)\n\n    if sys.version_info >= (3, 12):\n        def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ...  # pyright: ignore[reportInconsistentConstructor]\n    else:\n        def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ...\n\n    @classmethod\n    def cwd(cls) -> Self: ...\n    if sys.version_info >= (3, 10):\n        def stat(self, *, follow_symlinks: bool = True) -> stat_result: ...\n        def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ...\n    else:\n        def stat(self) -> stat_result: ...\n        def chmod(self, mode: int) -> None: ...\n\n    if sys.version_info >= (3, 13):\n        @classmethod\n        def from_uri(cls, uri: str) -> Self: ...\n        def is_dir(self, *, follow_symlinks: bool = True) -> bool: ...\n        def is_file(self, *, follow_symlinks: bool = True) -> bool: ...\n        def read_text(\n            self, encoding: str | None = None, errors: str | None = None, newline: str | None = None\n        ) -> str: ...\n    else:\n        def __enter__(self) -> Self: ...\n        def __exit__(\n            self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None\n        ) -> None: ...\n        def is_dir(self) -> bool: ...\n        def is_file(self) -> bool: ...\n        def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ...\n\n    if sys.version_info >= (3, 13):\n        def glob(\n            self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False\n        ) -> Iterator[Self]: ...\n        def rglob(\n            self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False\n        ) -> Iterator[Self]: ...\n    elif sys.version_info >= (3, 12):\n        def glob(self, pattern: str, *, case_sensitive: bool | None = None) -> Generator[Self, None, None]: ...\n        def rglob(self, pattern: str, *, case_sensitive: bool | None = None) -> Generator[Self, None, None]: ...\n    else:\n        def glob(self, pattern: str) -> Generator[Self, None, None]: ...\n        def rglob(self, pattern: str) -> Generator[Self, None, None]: ...\n\n    if sys.version_info >= (3, 12):\n        def exists(self, *, follow_symlinks: bool = True) -> bool: ...\n    else:\n        def exists(self) -> bool: ...\n\n    def is_symlink(self) -> bool: ...\n    def is_socket(self) -> bool: ...\n    def is_fifo(self) -> bool: ...\n    def is_block_device(self) -> bool: ...\n    def is_char_device(self) -> bool: ...\n    if sys.version_info >= (3, 12):\n        def is_junction(self) -> bool: ...\n\n    def iterdir(self) -> Generator[Self, None, None]: ...\n    def lchmod(self, mode: int) -> None: ...\n    def lstat(self) -> stat_result: ...\n    def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ...\n\n    if sys.version_info >= (3, 14):\n        @property\n        def info(self) -> PathInfo: ...\n        @overload\n        def move_into(self, target_dir: _PathT) -> _PathT: ...  # type: ignore[overload-overlap]\n        @overload\n        def move_into(self, target_dir: StrPath) -> Self: ...  # type: ignore[overload-overlap]\n        @overload\n        def move(self, target: _PathT) -> _PathT: ...  # type: ignore[overload-overlap]\n        @overload\n        def move(self, target: StrPath) -> Self: ...  # type: ignore[overload-overlap]\n        @overload\n        def copy_into(\n            self, target_dir: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False\n        ) -> _PathT: ...  # type: ignore[overload-overlap]\n        @overload\n        def copy_into(\n            self, target_dir: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False\n        ) -> Self: ...  # type: ignore[overload-overlap]\n        @overload\n        def copy(self, target: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ...  # type: ignore[overload-overlap]\n        @overload\n        def copy(self, target: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> Self: ...  # type: ignore[overload-overlap]\n\n    # Adapted from builtins.open\n    # Text mode: always returns a TextIOWrapper\n    # The Traversable .open in stdlib/importlib/abc.pyi should be kept in sync with this.\n    @overload\n    def open(\n        self,\n        mode: OpenTextMode = 'r',\n        buffering: int = -1,\n        encoding: str | None = None,\n        errors: str | None = None,\n        newline: str | None = None,\n    ) -> TextIOWrapper: ...\n    # Unbuffered binary mode: returns a FileIO\n    @overload\n    def open(\n        self,\n        mode: OpenBinaryMode,\n        buffering: Literal[0],\n        encoding: None = None,\n        errors: None = None,\n        newline: None = None,\n    ) -> FileIO: ...\n    # Buffering is on: return BufferedRandom, BufferedReader, or BufferedWriter\n    @overload\n    def open(\n        self,\n        mode: OpenBinaryModeUpdating,\n        buffering: Literal[-1, 1] = -1,\n        encoding: None = None,\n        errors: None = None,\n        newline: None = None,\n    ) -> BufferedRandom: ...\n    @overload\n    def open(\n        self,\n        mode: OpenBinaryModeWriting,\n        buffering: Literal[-1, 1] = -1,\n        encoding: None = None,\n        errors: None = None,\n        newline: None = None,\n    ) -> BufferedWriter: ...\n    @overload\n    def open(\n        self,\n        mode: OpenBinaryModeReading,\n        buffering: Literal[-1, 1] = -1,\n        encoding: None = None,\n        errors: None = None,\n        newline: None = None,\n    ) -> BufferedReader: ...\n    # Buffering cannot be determined: fall back to BinaryIO\n    @overload\n    def open(\n        self,\n        mode: OpenBinaryMode,\n        buffering: int = -1,\n        encoding: None = None,\n        errors: None = None,\n        newline: None = None,\n    ) -> BinaryIO: ...\n    # Fallback if mode is not specified\n    @overload\n    def open(\n        self,\n        mode: str,\n        buffering: int = -1,\n        encoding: str | None = None,\n        errors: str | None = None,\n        newline: str | None = None,\n    ) -> IO[Any]: ...\n\n    # These methods do \"exist\" on Windows, but they always raise NotImplementedError.\n    if sys.platform == 'win32':\n        if sys.version_info >= (3, 13):\n            # raises UnsupportedOperation:\n            def owner(self: Never, *, follow_symlinks: bool = True) -> str: ...  # type: ignore[misc]\n            def group(self: Never, *, follow_symlinks: bool = True) -> str: ...  # type: ignore[misc]\n        else:\n            def owner(self: Never) -> str: ...  # type: ignore[misc]\n            def group(self: Never) -> str: ...  # type: ignore[misc]\n    else:\n        if sys.version_info >= (3, 13):\n            def owner(self, *, follow_symlinks: bool = True) -> str: ...\n            def group(self, *, follow_symlinks: bool = True) -> str: ...\n        else:\n            def owner(self) -> str: ...\n            def group(self) -> str: ...\n\n    # This method does \"exist\" on Windows on <3.12, but always raises NotImplementedError\n    # On py312+, it works properly on Windows, as with all other platforms\n    if sys.platform == 'win32' and sys.version_info < (3, 12):\n        def is_mount(self: Never) -> bool: ...  # type: ignore[misc]\n    else:\n        def is_mount(self) -> bool: ...\n\n    def readlink(self) -> Self: ...\n\n    if sys.version_info >= (3, 10):\n        def rename(self, target: StrPath) -> Self: ...\n        def replace(self, target: StrPath) -> Self: ...\n    else:\n        def rename(self, target: str | PurePath) -> Self: ...\n        def replace(self, target: str | PurePath) -> Self: ...\n\n    def resolve(self, strict: bool = False) -> Self: ...\n    def rmdir(self) -> None: ...\n    def symlink_to(self, target: StrOrBytesPath, target_is_directory: bool = False) -> None: ...\n    if sys.version_info >= (3, 10):\n        def hardlink_to(self, target: StrOrBytesPath) -> None: ...\n\n    def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ...\n    def unlink(self, missing_ok: bool = False) -> None: ...\n    @classmethod\n    def home(cls) -> Self: ...\n    def absolute(self) -> Self: ...\n    def expanduser(self) -> Self: ...\n    def read_bytes(self) -> bytes: ...\n    def samefile(self, other_path: StrPath) -> bool: ...\n    def write_bytes(self, data: ReadableBuffer) -> int: ...\n    if sys.version_info >= (3, 10):\n        def write_text(\n            self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None\n        ) -> int: ...\n    else:\n        def write_text(self, data: str, encoding: str | None = None, errors: str | None = None) -> int: ...\n    if sys.version_info < (3, 12):\n        if sys.version_info >= (3, 10):\n            @deprecated('Deprecated since Python 3.10; removed in Python 3.12. Use `hardlink_to()` instead.')\n            def link_to(self, target: StrOrBytesPath) -> None: ...\n        else:\n            def link_to(self, target: StrOrBytesPath) -> None: ...\n    if sys.version_info >= (3, 12):\n        def walk(\n            self,\n            top_down: bool = True,\n            on_error: Callable[[OSError], object] | None = None,\n            follow_symlinks: bool = False,\n        ) -> Iterator[tuple[Self, list[str], list[str]]]: ...\n\n    def as_uri(self) -> str: ...\n\nclass PosixPath(Path, PurePosixPath):\n    __slots__ = ()\n\nclass WindowsPath(Path, PureWindowsPath):\n    __slots__ = ()\n\nif sys.version_info >= (3, 13):\n    class UnsupportedOperation(NotImplementedError): ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/pathlib/types.pyi",
    "content": "from typing import Protocol, runtime_checkable\n\n@runtime_checkable\nclass PathInfo(Protocol):\n    def exists(self, *, follow_symlinks: bool = True) -> bool: ...\n    def is_dir(self, *, follow_symlinks: bool = True) -> bool: ...\n    def is_file(self, *, follow_symlinks: bool = True) -> bool: ...\n    def is_symlink(self) -> bool: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/re.pyi",
    "content": "import enum\nimport sre_compile\nimport sre_constants\nimport sys\nfrom collections.abc import Callable, Iterator, Mapping\nfrom types import GenericAlias\nfrom typing import Any, AnyStr, Final, Generic, Literal, TypeVar, final, overload\n\nfrom _typeshed import MaybeNone, ReadableBuffer\nfrom typing_extensions import TypeAlias, deprecated\n\n__all__ = [\n    'match',\n    'fullmatch',\n    'search',\n    'sub',\n    'subn',\n    'split',\n    'findall',\n    'finditer',\n    'compile',\n    'purge',\n    'escape',\n    'error',\n    'A',\n    'I',\n    'L',\n    'M',\n    'S',\n    'X',\n    'U',\n    'ASCII',\n    'IGNORECASE',\n    'LOCALE',\n    'MULTILINE',\n    'DOTALL',\n    'VERBOSE',\n    'UNICODE',\n    'Match',\n    'Pattern',\n]\nif sys.version_info < (3, 13):\n    __all__ += ['template']\n\nif sys.version_info >= (3, 11):\n    __all__ += ['NOFLAG', 'RegexFlag']\n\nif sys.version_info >= (3, 13):\n    __all__ += ['PatternError']\n\n    PatternError = sre_constants.error\n\n_T = TypeVar('_T')\n\n# The implementation defines this in re._constants (version_info >= 3, 11) or\n# sre_constants. Typeshed has it here because its __module__ attribute is set to \"re\".\nclass error(Exception):\n    msg: str\n    pattern: str | bytes | None\n    pos: int | None\n    lineno: int\n    colno: int\n    def __init__(self, msg: str, pattern: str | bytes | None = None, pos: int | None = None) -> None: ...\n\n@final\nclass Match(Generic[AnyStr]):\n    @property\n    def pos(self) -> int: ...\n    @property\n    def endpos(self) -> int: ...\n    @property\n    def lastindex(self) -> int | None: ...\n    @property\n    def lastgroup(self) -> str | None: ...\n    @property\n    def string(self) -> AnyStr: ...\n\n    # The regular expression object whose match() or search() method produced\n    # this match instance.\n    @property\n    def re(self) -> Pattern[AnyStr]: ...\n    @overload\n    def expand(self: Match[str], template: str) -> str: ...\n    @overload\n    def expand(self: Match[bytes], template: ReadableBuffer) -> bytes: ...\n    @overload\n    def expand(self, template: AnyStr) -> AnyStr: ...\n    # group() returns \"AnyStr\" or \"AnyStr | None\", depending on the pattern.\n    @overload\n    def group(self, group: Literal[0] = 0, /) -> AnyStr: ...\n    @overload\n    def group(self, group: str | int, /) -> AnyStr | MaybeNone: ...\n    @overload\n    def group(self, group1: str | int, group2: str | int, /, *groups: str | int) -> tuple[AnyStr | MaybeNone, ...]: ...\n    # Each item of groups()'s return tuple is either \"AnyStr\" or\n    # \"AnyStr | None\", depending on the pattern.\n    @overload\n    def groups(self) -> tuple[AnyStr | MaybeNone, ...]: ...\n    @overload\n    def groups(self, default: _T) -> tuple[AnyStr | _T, ...]: ...\n    # Each value in groupdict()'s return dict is either \"AnyStr\" or\n    # \"AnyStr | None\", depending on the pattern.\n    @overload\n    def groupdict(self) -> dict[str, AnyStr | MaybeNone]: ...\n    @overload\n    def groupdict(self, default: _T) -> dict[str, AnyStr | _T]: ...\n    def start(self, group: int | str = 0, /) -> int: ...\n    def end(self, group: int | str = 0, /) -> int: ...\n    def span(self, group: int | str = 0, /) -> tuple[int, int]: ...\n    @property\n    def regs(self) -> tuple[tuple[int, int], ...]: ...  # undocumented\n    # __getitem__() returns \"AnyStr\" or \"AnyStr | None\", depending on the pattern.\n    @overload\n    def __getitem__(self, key: Literal[0], /) -> AnyStr: ...\n    @overload\n    def __getitem__(self, key: int | str, /) -> AnyStr | MaybeNone: ...\n    def __copy__(self) -> Match[AnyStr]: ...\n    def __deepcopy__(self, memo: Any, /) -> Match[AnyStr]: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n@final\nclass Pattern(Generic[AnyStr]):\n    @property\n    def flags(self) -> int: ...\n    @property\n    def groupindex(self) -> Mapping[str, int]: ...\n    @property\n    def groups(self) -> int: ...\n    @property\n    def pattern(self) -> AnyStr: ...\n    @overload\n    def search(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Match[str] | None: ...\n    @overload\n    def search(\n        self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize\n    ) -> Match[bytes] | None: ...\n    @overload\n    def search(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Match[AnyStr] | None: ...\n    @overload\n    def match(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Match[str] | None: ...\n    @overload\n    def match(\n        self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize\n    ) -> Match[bytes] | None: ...\n    @overload\n    def match(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Match[AnyStr] | None: ...\n    @overload\n    def fullmatch(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Match[str] | None: ...\n    @overload\n    def fullmatch(\n        self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize\n    ) -> Match[bytes] | None: ...\n    @overload\n    def fullmatch(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Match[AnyStr] | None: ...\n    @overload\n    def split(self: Pattern[str], string: str, maxsplit: int = 0) -> list[str | MaybeNone]: ...\n    @overload\n    def split(self: Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0) -> list[bytes | MaybeNone]: ...\n    @overload\n    def split(self, string: AnyStr, maxsplit: int = 0) -> list[AnyStr | MaybeNone]: ...\n    # return type depends on the number of groups in the pattern\n    @overload\n    def findall(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> list[Any]: ...\n    @overload\n    def findall(self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize) -> list[Any]: ...\n    @overload\n    def findall(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> list[AnyStr]: ...\n    @overload\n    def finditer(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Iterator[Match[str]]: ...\n    @overload\n    def finditer(\n        self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize\n    ) -> Iterator[Match[bytes]]: ...\n    @overload\n    def finditer(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Iterator[Match[AnyStr]]: ...\n    @overload\n    def sub(self: Pattern[str], repl: str | Callable[[Match[str]], str], string: str, count: int = 0) -> str: ...\n    @overload\n    def sub(\n        self: Pattern[bytes],\n        repl: ReadableBuffer | Callable[[Match[bytes]], ReadableBuffer],\n        string: ReadableBuffer,\n        count: int = 0,\n    ) -> bytes: ...\n    @overload\n    def sub(self, repl: AnyStr | Callable[[Match[AnyStr]], AnyStr], string: AnyStr, count: int = 0) -> AnyStr: ...\n    @overload\n    def subn(\n        self: Pattern[str], repl: str | Callable[[Match[str]], str], string: str, count: int = 0\n    ) -> tuple[str, int]: ...\n    @overload\n    def subn(\n        self: Pattern[bytes],\n        repl: ReadableBuffer | Callable[[Match[bytes]], ReadableBuffer],\n        string: ReadableBuffer,\n        count: int = 0,\n    ) -> tuple[bytes, int]: ...\n    @overload\n    def subn(\n        self, repl: AnyStr | Callable[[Match[AnyStr]], AnyStr], string: AnyStr, count: int = 0\n    ) -> tuple[AnyStr, int]: ...\n    def __copy__(self) -> Pattern[AnyStr]: ...\n    def __deepcopy__(self, memo: Any, /) -> Pattern[AnyStr]: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n# ----- re variables and constants -----\n\nclass RegexFlag(enum.IntFlag):\n    A = sre_compile.SRE_FLAG_ASCII\n    ASCII = A\n    DEBUG = sre_compile.SRE_FLAG_DEBUG\n    I = sre_compile.SRE_FLAG_IGNORECASE\n    IGNORECASE = I\n    L = sre_compile.SRE_FLAG_LOCALE\n    LOCALE = L\n    M = sre_compile.SRE_FLAG_MULTILINE\n    MULTILINE = M\n    S = sre_compile.SRE_FLAG_DOTALL\n    DOTALL = S\n    X = sre_compile.SRE_FLAG_VERBOSE\n    VERBOSE = X\n    U = sre_compile.SRE_FLAG_UNICODE\n    UNICODE = U\n    if sys.version_info < (3, 13):\n        T = sre_compile.SRE_FLAG_TEMPLATE\n        TEMPLATE = T\n    if sys.version_info >= (3, 11):\n        NOFLAG = 0\n\nA: Final = RegexFlag.A\nASCII: Final = RegexFlag.ASCII\nDEBUG: Final = RegexFlag.DEBUG\nI: Final = RegexFlag.I\nIGNORECASE: Final = RegexFlag.IGNORECASE\nL: Final = RegexFlag.L\nLOCALE: Final = RegexFlag.LOCALE\nM: Final = RegexFlag.M\nMULTILINE: Final = RegexFlag.MULTILINE\nS: Final = RegexFlag.S\nDOTALL: Final = RegexFlag.DOTALL\nX: Final = RegexFlag.X\nVERBOSE: Final = RegexFlag.VERBOSE\nU: Final = RegexFlag.U\nUNICODE: Final = RegexFlag.UNICODE\nif sys.version_info < (3, 13):\n    T: Final = RegexFlag.T\n    TEMPLATE: Final = RegexFlag.TEMPLATE\nif sys.version_info >= (3, 11):\n    NOFLAG: Final = RegexFlag.NOFLAG\n_FlagsType: TypeAlias = int | RegexFlag\n\n# Type-wise the compile() overloads are unnecessary, they could also be modeled using\n# unions in the parameter types. However mypy has a bug regarding TypeVar\n# constraints (https://github.com/python/mypy/issues/11880),\n# which limits us here because AnyStr is a constrained TypeVar.\n\n# pattern arguments do *not* accept arbitrary buffers such as bytearray,\n# because the pattern must be hashable.\n@overload\ndef compile(pattern: AnyStr, flags: _FlagsType = 0) -> Pattern[AnyStr]: ...\n@overload\ndef compile(pattern: Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ...\n@overload\ndef search(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ...\n@overload\ndef search(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Match[bytes] | None: ...\n@overload\ndef match(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ...\n@overload\ndef match(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Match[bytes] | None: ...\n@overload\ndef fullmatch(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ...\n@overload\ndef fullmatch(\n    pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0\n) -> Match[bytes] | None: ...\n@overload\ndef split(\n    pattern: str | Pattern[str], string: str, maxsplit: int = 0, flags: _FlagsType = 0\n) -> list[str | MaybeNone]: ...\n@overload\ndef split(\n    pattern: bytes | Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0, flags: _FlagsType = 0\n) -> list[bytes | MaybeNone]: ...\n@overload\ndef findall(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> list[Any]: ...\n@overload\ndef findall(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> list[Any]: ...\n@overload\ndef finditer(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Iterator[Match[str]]: ...\n@overload\ndef finditer(\n    pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0\n) -> Iterator[Match[bytes]]: ...\n@overload\ndef sub(\n    pattern: str | Pattern[str],\n    repl: str | Callable[[Match[str]], str],\n    string: str,\n    count: int = 0,\n    flags: _FlagsType = 0,\n) -> str: ...\n@overload\ndef sub(\n    pattern: bytes | Pattern[bytes],\n    repl: ReadableBuffer | Callable[[Match[bytes]], ReadableBuffer],\n    string: ReadableBuffer,\n    count: int = 0,\n    flags: _FlagsType = 0,\n) -> bytes: ...\n@overload\ndef subn(\n    pattern: str | Pattern[str],\n    repl: str | Callable[[Match[str]], str],\n    string: str,\n    count: int = 0,\n    flags: _FlagsType = 0,\n) -> tuple[str, int]: ...\n@overload\ndef subn(\n    pattern: bytes | Pattern[bytes],\n    repl: ReadableBuffer | Callable[[Match[bytes]], ReadableBuffer],\n    string: ReadableBuffer,\n    count: int = 0,\n    flags: _FlagsType = 0,\n) -> tuple[bytes, int]: ...\ndef escape(pattern: AnyStr) -> AnyStr: ...\ndef purge() -> None: ...\n\nif sys.version_info < (3, 13):\n    if sys.version_info >= (3, 11):\n        @deprecated('Deprecated since Python 3.11; removed in Python 3.13. Use `re.compile()` instead.')\n        def template(pattern: AnyStr | Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ...  # undocumented\n    else:\n        def template(pattern: AnyStr | Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ...  # undocumented\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/sys.pyi",
    "content": "from typing import Any, Final, Literal, TextIO, final, type_check_only\n\nfrom _typeshed import MaybeNone, structseq\nfrom typing_extensions import TypeAlias\n\n# stdin: TextIO | MaybeNone\nstdout: TextIO | MaybeNone\nstderr: TextIO | MaybeNone\n\nversion: str\n\n# Type alias used as a mixin for structseq classes that cannot be instantiated at runtime\n# This can't be represented in the type system, so we just use `structseq[Any]`\n_UninstantiableStructseq: TypeAlias = structseq[Any]\n_ReleaseLevel: TypeAlias = Literal['alpha', 'beta', 'candidate', 'final']\n\n@final\n@type_check_only\nclass _version_info(_UninstantiableStructseq, tuple[int, int, int, _ReleaseLevel, int]):\n    __match_args__: Final = ('major', 'minor', 'micro', 'releaselevel', 'serial')\n\n    @property\n    def major(self) -> int: ...\n    @property\n    def minor(self) -> int: ...\n    @property\n    def micro(self) -> int: ...\n    @property\n    def releaselevel(self) -> _ReleaseLevel: ...\n    @property\n    def serial(self) -> int: ...\n\nversion_info: _version_info\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/types.pyi",
    "content": "import sys\nfrom collections.abc import (\n    AsyncGenerator,\n    Awaitable,\n    Callable,\n    Coroutine,\n    Generator,\n    ItemsView,\n    Iterable,\n    Iterator,\n    KeysView,\n    Mapping,\n    MutableSequence,\n    ValuesView,\n)\nfrom importlib.machinery import ModuleSpec\nfrom typing import Any, ClassVar, Literal, TypeVar, final, overload\n\nfrom _typeshed import AnnotationForm, MaybeNone, SupportsKeysAndGetItem\nfrom _typeshed.importlib import LoaderProtocol\nfrom typing_extensions import ParamSpec, Self, TypeAliasType, TypeVarTuple, deprecated, disjoint_base\n\nif sys.version_info >= (3, 14):\n    from _typeshed import AnnotateFunc\n\n__all__ = [\n    'FunctionType',\n    'LambdaType',\n    'CodeType',\n    'MappingProxyType',\n    'SimpleNamespace',\n    'GeneratorType',\n    'CoroutineType',\n    'AsyncGeneratorType',\n    'MethodType',\n    'BuiltinFunctionType',\n    'ModuleType',\n    'TracebackType',\n    'FrameType',\n    'GetSetDescriptorType',\n    'MemberDescriptorType',\n    'new_class',\n    'prepare_class',\n    'DynamicClassAttribute',\n    'coroutine',\n    'BuiltinMethodType',\n    'ClassMethodDescriptorType',\n    'MethodDescriptorType',\n    'MethodWrapperType',\n    'WrapperDescriptorType',\n    'resolve_bases',\n    'CellType',\n    'GenericAlias',\n]\n\nif sys.version_info >= (3, 10):\n    __all__ += ['EllipsisType', 'NoneType', 'NotImplementedType', 'UnionType']\n\nif sys.version_info >= (3, 12):\n    __all__ += ['get_original_bases']\n\nif sys.version_info >= (3, 13):\n    __all__ += ['CapsuleType']\n\n# Note, all classes \"defined\" here require special handling.\n\n_T1 = TypeVar('_T1')\n_T2 = TypeVar('_T2')\n_KT_co = TypeVar('_KT_co', covariant=True)\n_VT_co = TypeVar('_VT_co', covariant=True)\n\n# Make sure this class definition stays roughly in line with `builtins.function`\n@final\nclass FunctionType:\n    @property\n    def __closure__(self) -> tuple[CellType, ...] | None: ...\n    __code__: CodeType\n    __defaults__: tuple[Any, ...] | None\n    __dict__: dict[str, Any]\n    @property\n    def __globals__(self) -> dict[str, Any]: ...\n    __name__: str\n    __qualname__: str\n    __annotations__: dict[str, AnnotationForm]\n    if sys.version_info >= (3, 14):\n        __annotate__: AnnotateFunc | None\n    __kwdefaults__: dict[str, Any] | None\n    if sys.version_info >= (3, 10):\n        @property\n        def __builtins__(self) -> dict[str, Any]: ...\n    if sys.version_info >= (3, 12):\n        __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]\n\n    __module__: str\n    if sys.version_info >= (3, 13):\n        def __new__(\n            cls,\n            code: CodeType,\n            globals: dict[str, Any],\n            name: str | None = None,\n            argdefs: tuple[object, ...] | None = None,\n            closure: tuple[CellType, ...] | None = None,\n            kwdefaults: dict[str, object] | None = None,\n        ) -> Self: ...\n    else:\n        def __new__(\n            cls,\n            code: CodeType,\n            globals: dict[str, Any],\n            name: str | None = None,\n            argdefs: tuple[object, ...] | None = None,\n            closure: tuple[CellType, ...] | None = None,\n        ) -> Self: ...\n\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n    @overload\n    def __get__(self, instance: None, owner: type, /) -> FunctionType: ...\n    @overload\n    def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...\n\nLambdaType = FunctionType\n\n@final\nclass CodeType:\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    @property\n    def co_argcount(self) -> int: ...\n    @property\n    def co_posonlyargcount(self) -> int: ...\n    @property\n    def co_kwonlyargcount(self) -> int: ...\n    @property\n    def co_nlocals(self) -> int: ...\n    @property\n    def co_stacksize(self) -> int: ...\n    @property\n    def co_flags(self) -> int: ...\n    @property\n    def co_code(self) -> bytes: ...\n    @property\n    def co_consts(self) -> tuple[Any, ...]: ...\n    @property\n    def co_names(self) -> tuple[str, ...]: ...\n    @property\n    def co_varnames(self) -> tuple[str, ...]: ...\n    @property\n    def co_filename(self) -> str: ...\n    @property\n    def co_name(self) -> str: ...\n    @property\n    def co_firstlineno(self) -> int: ...\n    if sys.version_info >= (3, 10):\n        @property\n        @deprecated('Deprecated since Python 3.10; will be removed in Python 3.15. Use `CodeType.co_lines()` instead.')\n        def co_lnotab(self) -> bytes: ...\n    else:\n        @property\n        def co_lnotab(self) -> bytes: ...\n\n    @property\n    def co_freevars(self) -> tuple[str, ...]: ...\n    @property\n    def co_cellvars(self) -> tuple[str, ...]: ...\n    if sys.version_info >= (3, 10):\n        @property\n        def co_linetable(self) -> bytes: ...\n        def co_lines(self) -> Iterator[tuple[int, int, int | None]]: ...\n    if sys.version_info >= (3, 11):\n        @property\n        def co_exceptiontable(self) -> bytes: ...\n        @property\n        def co_qualname(self) -> str: ...\n        def co_positions(self) -> Iterable[tuple[int | None, int | None, int | None, int | None]]: ...\n    if sys.version_info >= (3, 14):\n        def co_branches(self) -> Iterator[tuple[int, int, int]]: ...\n\n    if sys.version_info >= (3, 11):\n        def __new__(\n            cls,\n            argcount: int,\n            posonlyargcount: int,\n            kwonlyargcount: int,\n            nlocals: int,\n            stacksize: int,\n            flags: int,\n            codestring: bytes,\n            constants: tuple[object, ...],\n            names: tuple[str, ...],\n            varnames: tuple[str, ...],\n            filename: str,\n            name: str,\n            qualname: str,\n            firstlineno: int,\n            linetable: bytes,\n            exceptiontable: bytes,\n            freevars: tuple[str, ...] = ...,\n            cellvars: tuple[str, ...] = ...,\n            /,\n        ) -> Self: ...\n    elif sys.version_info >= (3, 10):\n        def __new__(\n            cls,\n            argcount: int,\n            posonlyargcount: int,\n            kwonlyargcount: int,\n            nlocals: int,\n            stacksize: int,\n            flags: int,\n            codestring: bytes,\n            constants: tuple[object, ...],\n            names: tuple[str, ...],\n            varnames: tuple[str, ...],\n            filename: str,\n            name: str,\n            firstlineno: int,\n            linetable: bytes,\n            freevars: tuple[str, ...] = ...,\n            cellvars: tuple[str, ...] = ...,\n            /,\n        ) -> Self: ...\n    else:\n        def __new__(\n            cls,\n            argcount: int,\n            posonlyargcount: int,\n            kwonlyargcount: int,\n            nlocals: int,\n            stacksize: int,\n            flags: int,\n            codestring: bytes,\n            constants: tuple[object, ...],\n            names: tuple[str, ...],\n            varnames: tuple[str, ...],\n            filename: str,\n            name: str,\n            firstlineno: int,\n            lnotab: bytes,\n            freevars: tuple[str, ...] = ...,\n            cellvars: tuple[str, ...] = ...,\n            /,\n        ) -> Self: ...\n    if sys.version_info >= (3, 11):\n        def replace(\n            self,\n            *,\n            co_argcount: int = -1,\n            co_posonlyargcount: int = -1,\n            co_kwonlyargcount: int = -1,\n            co_nlocals: int = -1,\n            co_stacksize: int = -1,\n            co_flags: int = -1,\n            co_firstlineno: int = -1,\n            co_code: bytes = ...,\n            co_consts: tuple[object, ...] = ...,\n            co_names: tuple[str, ...] = ...,\n            co_varnames: tuple[str, ...] = ...,\n            co_freevars: tuple[str, ...] = ...,\n            co_cellvars: tuple[str, ...] = ...,\n            co_filename: str = ...,\n            co_name: str = ...,\n            co_qualname: str = ...,\n            co_linetable: bytes = ...,\n            co_exceptiontable: bytes = ...,\n        ) -> Self: ...\n    elif sys.version_info >= (3, 10):\n        def replace(\n            self,\n            *,\n            co_argcount: int = -1,\n            co_posonlyargcount: int = -1,\n            co_kwonlyargcount: int = -1,\n            co_nlocals: int = -1,\n            co_stacksize: int = -1,\n            co_flags: int = -1,\n            co_firstlineno: int = -1,\n            co_code: bytes = ...,\n            co_consts: tuple[object, ...] = ...,\n            co_names: tuple[str, ...] = ...,\n            co_varnames: tuple[str, ...] = ...,\n            co_freevars: tuple[str, ...] = ...,\n            co_cellvars: tuple[str, ...] = ...,\n            co_filename: str = ...,\n            co_name: str = ...,\n            co_linetable: bytes = ...,\n        ) -> Self: ...\n    else:\n        def replace(\n            self,\n            *,\n            co_argcount: int = -1,\n            co_posonlyargcount: int = -1,\n            co_kwonlyargcount: int = -1,\n            co_nlocals: int = -1,\n            co_stacksize: int = -1,\n            co_flags: int = -1,\n            co_firstlineno: int = -1,\n            co_code: bytes = ...,\n            co_consts: tuple[object, ...] = ...,\n            co_names: tuple[str, ...] = ...,\n            co_varnames: tuple[str, ...] = ...,\n            co_freevars: tuple[str, ...] = ...,\n            co_cellvars: tuple[str, ...] = ...,\n            co_filename: str = ...,\n            co_name: str = ...,\n            co_lnotab: bytes = ...,\n        ) -> Self: ...\n\n    if sys.version_info >= (3, 13):\n        __replace__ = replace\n\n@final\nclass MappingProxyType(Mapping[_KT_co, _VT_co]):  # type: ignore[type-var]  # pyright: ignore[reportInvalidTypeArguments]\n    __hash__: ClassVar[None]  # type: ignore[assignment]\n    def __new__(cls, mapping: SupportsKeysAndGetItem[_KT_co, _VT_co]) -> Self: ...\n    def __getitem__(self, key: _KT_co, /) -> _VT_co: ...  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n    def __iter__(self) -> Iterator[_KT_co]: ...\n    def __len__(self) -> int: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def copy(self) -> dict[_KT_co, _VT_co]: ...\n    def keys(self) -> KeysView[_KT_co]: ...\n    def values(self) -> ValuesView[_VT_co]: ...\n    def items(self) -> ItemsView[_KT_co, _VT_co]: ...\n    @overload\n    def get(self, key: _KT_co, /) -> _VT_co | None: ...  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter\n    @overload\n    def get(self, key: _KT_co, default: _VT_co, /) -> _VT_co: ...  # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter\n    @overload\n    def get(self, key: _KT_co, default: _T2, /) -> _VT_co | _T2: ...  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n    def __reversed__(self) -> Iterator[_KT_co]: ...\n    def __or__(self, value: Mapping[_T1, _T2], /) -> dict[_KT_co | _T1, _VT_co | _T2]: ...\n    def __ror__(self, value: Mapping[_T1, _T2], /) -> dict[_KT_co | _T1, _VT_co | _T2]: ...\n\nif sys.version_info >= (3, 12):\n    @disjoint_base\n    class SimpleNamespace:\n        __hash__: ClassVar[None]  # type: ignore[assignment]\n        if sys.version_info >= (3, 13):\n            def __init__(\n                self, mapping_or_iterable: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), /, **kwargs: Any\n            ) -> None: ...\n        else:\n            def __init__(self, **kwargs: Any) -> None: ...\n\n        def __eq__(self, value: object, /) -> bool: ...\n        def __getattribute__(self, name: str, /) -> Any: ...\n        def __setattr__(self, name: str, value: Any, /) -> None: ...\n        def __delattr__(self, name: str, /) -> None: ...\n        if sys.version_info >= (3, 13):\n            def __replace__(self, **kwargs: Any) -> Self: ...\n\nelse:\n    class SimpleNamespace:\n        __hash__: ClassVar[None]  # type: ignore[assignment]\n        def __init__(self, **kwargs: Any) -> None: ...\n        def __eq__(self, value: object, /) -> bool: ...\n        def __getattribute__(self, name: str, /) -> Any: ...\n        def __setattr__(self, name: str, value: Any, /) -> None: ...\n        def __delattr__(self, name: str, /) -> None: ...\n\n@disjoint_base\nclass ModuleType:\n    __name__: str\n    __file__: str | None\n    @property\n    def __dict__(self) -> dict[str, Any]: ...  # type: ignore[override]\n    __loader__: LoaderProtocol | None\n    __package__: str | None\n    __path__: MutableSequence[str]\n    __spec__: ModuleSpec | None\n    # N.B. Although this is the same type as `builtins.object.__doc__`,\n    # it is deliberately redeclared here. Most symbols declared in the namespace\n    # of `types.ModuleType` are available as \"implicit globals\" within a module's\n    # namespace, but this is not true for symbols declared in the namespace of `builtins.object`.\n    # Redeclaring `__doc__` here helps some type checkers understand that `__doc__` is available\n    # as an implicit global in all modules, similar to `__name__`, `__file__`, `__spec__`, etc.\n    __doc__: str | None\n    __annotations__: dict[str, AnnotationForm]\n    if sys.version_info >= (3, 14):\n        __annotate__: AnnotateFunc | None\n\n    def __init__(self, name: str, doc: str | None = ...) -> None: ...\n    # __getattr__ doesn't exist at runtime,\n    # but having it here in typeshed makes dynamic imports\n    # using `builtins.__import__` or `importlib.import_module` less painful\n    def __getattr__(self, name: str) -> Any: ...\n\n@final\nclass CellType:\n    def __new__(cls, contents: object = ..., /) -> Self: ...\n    __hash__: ClassVar[None]  # type: ignore[assignment]\n    cell_contents: Any\n\n_YieldT_co = TypeVar('_YieldT_co', covariant=True)\n_SendT_contra = TypeVar('_SendT_contra', contravariant=True, default=None)\n_ReturnT_co = TypeVar('_ReturnT_co', covariant=True, default=None)\n\n@final\nclass GeneratorType(Generator[_YieldT_co, _SendT_contra, _ReturnT_co]):\n    @property\n    def gi_code(self) -> CodeType: ...\n    @property\n    def gi_frame(self) -> FrameType | None: ...\n    @property\n    def gi_running(self) -> bool: ...\n    @property\n    def gi_yieldfrom(self) -> Iterator[_YieldT_co] | None: ...\n    if sys.version_info >= (3, 11):\n        @property\n        def gi_suspended(self) -> bool: ...\n    __name__: str\n    __qualname__: str\n    def __iter__(self) -> Self: ...\n    def __next__(self) -> _YieldT_co: ...\n    def send(self, arg: _SendT_contra, /) -> _YieldT_co: ...\n    @overload\n    def throw(\n        self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., /\n    ) -> _YieldT_co: ...\n    @overload\n    def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ...\n    if sys.version_info >= (3, 13):\n        def __class_getitem__(cls, item: Any, /) -> Any: ...\n\n@final\nclass AsyncGeneratorType(AsyncGenerator[_YieldT_co, _SendT_contra]):\n    @property\n    def ag_await(self) -> Awaitable[Any] | None: ...\n    @property\n    def ag_code(self) -> CodeType: ...\n    @property\n    def ag_frame(self) -> FrameType | None: ...\n    @property\n    def ag_running(self) -> bool: ...\n    __name__: str\n    __qualname__: str\n    if sys.version_info >= (3, 12):\n        @property\n        def ag_suspended(self) -> bool: ...\n\n    def __aiter__(self) -> Self: ...\n    def __anext__(self) -> Coroutine[Any, Any, _YieldT_co]: ...\n    def asend(self, val: _SendT_contra, /) -> Coroutine[Any, Any, _YieldT_co]: ...\n    @overload\n    async def athrow(\n        self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., /\n    ) -> _YieldT_co: ...\n    @overload\n    async def athrow(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ...\n    def aclose(self) -> Coroutine[Any, Any, None]: ...\n    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...\n\n# Non-default variations to accommodate coroutines\n_SendT_nd_contra = TypeVar('_SendT_nd_contra', contravariant=True)\n_ReturnT_nd_co = TypeVar('_ReturnT_nd_co', covariant=True)\n\n@final\nclass CoroutineType(Coroutine[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co]):\n    __name__: str\n    __qualname__: str\n    @property\n    def cr_await(self) -> Any | None: ...\n    @property\n    def cr_code(self) -> CodeType: ...\n    @property\n    def cr_frame(self) -> FrameType | None: ...\n    @property\n    def cr_running(self) -> bool: ...\n    @property\n    def cr_origin(self) -> tuple[tuple[str, int, str], ...] | None: ...\n    if sys.version_info >= (3, 11):\n        @property\n        def cr_suspended(self) -> bool: ...\n\n    def close(self) -> None: ...\n    def __await__(self) -> Generator[Any, None, _ReturnT_nd_co]: ...\n    def send(self, arg: _SendT_nd_contra, /) -> _YieldT_co: ...\n    @overload\n    def throw(\n        self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., /\n    ) -> _YieldT_co: ...\n    @overload\n    def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ...\n    if sys.version_info >= (3, 13):\n        def __class_getitem__(cls, item: Any, /) -> Any: ...\n\n@final\nclass MethodType:\n    @property\n    def __closure__(self) -> tuple[CellType, ...] | None: ...  # inherited from the added function\n    @property\n    def __code__(self) -> CodeType: ...  # inherited from the added function\n    @property\n    def __defaults__(self) -> tuple[Any, ...] | None: ...  # inherited from the added function\n    @property\n    def __func__(self) -> Callable[..., Any]: ...\n    @property\n    def __self__(self) -> object: ...\n    @property\n    def __name__(self) -> str: ...  # inherited from the added function\n    @property\n    def __qualname__(self) -> str: ...  # inherited from the added function\n    def __new__(cls, func: Callable[..., Any], instance: object, /) -> Self: ...\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n\n    if sys.version_info >= (3, 13):\n        def __get__(self, instance: object, owner: type | None = None, /) -> Self: ...\n\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n\n@final\nclass BuiltinFunctionType:\n    @property\n    def __self__(self) -> object | ModuleType: ...\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n\nBuiltinMethodType = BuiltinFunctionType\n\n@final\nclass WrapperDescriptorType:\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    @property\n    def __objclass__(self) -> type: ...\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n    def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...\n\n@final\nclass MethodWrapperType:\n    @property\n    def __self__(self) -> object: ...\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    @property\n    def __objclass__(self) -> type: ...\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __ne__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n\n@final\nclass MethodDescriptorType:\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    @property\n    def __objclass__(self) -> type: ...\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n    def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...\n\n@final\nclass ClassMethodDescriptorType:\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    @property\n    def __objclass__(self) -> type: ...\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n    def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...\n\n@final\nclass TracebackType:\n    def __new__(cls, tb_next: TracebackType | None, tb_frame: FrameType, tb_lasti: int, tb_lineno: int) -> Self: ...\n    tb_next: TracebackType | None\n    # the rest are read-only\n    @property\n    def tb_frame(self) -> FrameType: ...\n    @property\n    def tb_lasti(self) -> int: ...\n    @property\n    def tb_lineno(self) -> int: ...\n\n@final\nclass FrameType:\n    @property\n    def f_back(self) -> FrameType | None: ...\n    @property\n    def f_builtins(self) -> dict[str, Any]: ...\n    @property\n    def f_code(self) -> CodeType: ...\n    @property\n    def f_globals(self) -> dict[str, Any]: ...\n    @property\n    def f_lasti(self) -> int: ...\n    # see discussion in #6769: f_lineno *can* sometimes be None,\n    # but you should probably file a bug report with CPython if you encounter it being None in the wild.\n    # An `int | None` annotation here causes too many false-positive errors, so applying `int | Any`.\n    @property\n    def f_lineno(self) -> int | MaybeNone: ...\n    @property\n    def f_locals(self) -> dict[str, Any]: ...\n    f_trace: Callable[[FrameType, str, Any], Any] | None\n    f_trace_lines: bool\n    f_trace_opcodes: bool\n    def clear(self) -> None: ...\n    if sys.version_info >= (3, 14):\n        @property\n        def f_generator(self) -> GeneratorType[Any, Any, Any] | CoroutineType[Any, Any, Any] | None: ...\n\n@final\nclass GetSetDescriptorType:\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    @property\n    def __objclass__(self) -> type: ...\n    def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...\n    def __set__(self, instance: Any, value: Any, /) -> None: ...\n    def __delete__(self, instance: Any, /) -> None: ...\n\n@final\nclass MemberDescriptorType:\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __qualname__(self) -> str: ...\n    @property\n    def __objclass__(self) -> type: ...\n    def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...\n    def __set__(self, instance: Any, value: Any, /) -> None: ...\n    def __delete__(self, instance: Any, /) -> None: ...\n\ndef new_class(\n    name: str,\n    bases: Iterable[object] = (),\n    kwds: dict[str, Any] | None = None,\n    exec_body: Callable[[dict[str, Any]], object] | None = None,\n) -> type: ...\ndef resolve_bases(bases: Iterable[object]) -> tuple[Any, ...]: ...\ndef prepare_class(\n    name: str, bases: tuple[type, ...] = (), kwds: dict[str, Any] | None = None\n) -> tuple[type, dict[str, Any], dict[str, Any]]: ...\n\nif sys.version_info >= (3, 12):\n    def get_original_bases(cls: type, /) -> tuple[Any, ...]: ...\n\n# Does not actually inherit from property, but saying it does makes sure that\n# pyright handles this class correctly.\nclass DynamicClassAttribute(property):\n    fget: Callable[[Any], Any] | None\n    fset: Callable[[Any, Any], object] | None  # type: ignore[assignment]\n    fdel: Callable[[Any], object] | None  # type: ignore[assignment]\n    overwrite_doc: bool\n    __isabstractmethod__: bool\n    def __init__(\n        self,\n        fget: Callable[[Any], Any] | None = None,\n        fset: Callable[[Any, Any], object] | None = None,\n        fdel: Callable[[Any], object] | None = None,\n        doc: str | None = None,\n    ) -> None: ...\n    def __get__(self, instance: Any, ownerclass: type | None = None) -> Any: ...\n    def __set__(self, instance: Any, value: Any) -> None: ...\n    def __delete__(self, instance: Any) -> None: ...\n    def getter(self, fget: Callable[[Any], Any]) -> DynamicClassAttribute: ...\n    def setter(self, fset: Callable[[Any, Any], object]) -> DynamicClassAttribute: ...\n    def deleter(self, fdel: Callable[[Any], object]) -> DynamicClassAttribute: ...\n\n_Fn = TypeVar('_Fn', bound=Callable[..., object])\n_R = TypeVar('_R')\n_P = ParamSpec('_P')\n\n# it's not really an Awaitable, but can be used in an await expression. Real type: Generator & Awaitable\n@overload\ndef coroutine(func: Callable[_P, Generator[Any, Any, _R]]) -> Callable[_P, Awaitable[_R]]: ...\n@overload\ndef coroutine(func: _Fn) -> _Fn: ...\n@disjoint_base\nclass GenericAlias:\n    @property\n    def __origin__(self) -> type | TypeAliasType: ...\n    @property\n    def __args__(self) -> tuple[Any, ...]: ...\n    @property\n    def __parameters__(self) -> tuple[Any, ...]: ...\n    def __new__(cls, origin: type, args: Any, /) -> Self: ...\n    def __getitem__(self, typeargs: Any, /) -> GenericAlias: ...\n    def __eq__(self, value: object, /) -> bool: ...\n    def __hash__(self) -> int: ...\n    def __mro_entries__(self, bases: Iterable[object], /) -> tuple[type, ...]: ...\n    if sys.version_info >= (3, 11):\n        @property\n        def __unpacked__(self) -> bool: ...\n        @property\n        def __typing_unpacked_tuple_args__(self) -> tuple[Any, ...] | None: ...\n    if sys.version_info >= (3, 10):\n        def __or__(self, value: Any, /) -> UnionType: ...\n        def __ror__(self, value: Any, /) -> UnionType: ...\n\n    # GenericAlias delegates attr access to `__origin__`\n    def __getattr__(self, name: str) -> Any: ...\n\nif sys.version_info >= (3, 10):\n    @final\n    class NoneType:\n        def __bool__(self) -> Literal[False]: ...\n\n    @final\n    class EllipsisType: ...\n\n    @final\n    class NotImplementedType(Any): ...\n\n    @final\n    class UnionType:\n        @property\n        def __args__(self) -> tuple[Any, ...]: ...\n        @property\n        def __parameters__(self) -> tuple[Any, ...]: ...\n        # `(int | str) | Literal[\"foo\"]` returns a generic alias to an instance of `_SpecialForm` (`Union`).\n        # Normally we'd express this using the return type of `_SpecialForm.__ror__`,\n        # but because `UnionType.__or__` accepts `Any`, type checkers will use\n        # the return type of `UnionType.__or__` to infer the result of this operation\n        # rather than `_SpecialForm.__ror__`. To mitigate this, we use `| Any`\n        # in the return type of `UnionType.__(r)or__`.\n        def __or__(self, value: Any, /) -> UnionType | Any: ...\n        def __ror__(self, value: Any, /) -> UnionType | Any: ...\n        def __eq__(self, value: object, /) -> bool: ...\n        def __hash__(self) -> int: ...\n        # you can only subscript a `UnionType` instance if at least one of the elements\n        # in the union is a generic alias instance that has a non-empty `__parameters__`\n        def __getitem__(self, parameters: Any, /) -> object: ...\n\nif sys.version_info >= (3, 13):\n    @final\n    class CapsuleType: ...\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/typing.pyi",
    "content": "# Since this module defines \"overload\" it is not recognized by Ruff as typing.overload\n# TODO: The collections import is required, otherwise mypy crashes.\n# https://github.com/python/mypy/issues/16744\nimport collections  # noqa: F401  # pyright: ignore[reportUnusedImport]\nimport sys\nfrom _collections_abc import dict_items, dict_keys, dict_values\nfrom abc import ABCMeta, abstractmethod\nfrom re import Match as Match, Pattern as Pattern\nfrom types import (\n    BuiltinFunctionType,\n    CodeType,\n    FunctionType,\n    GenericAlias,\n    MethodDescriptorType,\n    MethodType,\n    MethodWrapperType,\n    ModuleType,\n    TracebackType,\n    WrapperDescriptorType,\n)\n\nimport typing_extensions\nfrom _typeshed import (\n    IdentityFunction,\n    ReadableBuffer,\n    SupportsGetItem,\n    SupportsGetItemViewable,\n    SupportsKeysAndGetItem,\n    Viewable,\n)\nfrom typing_extensions import Never as _Never, ParamSpec as _ParamSpec, deprecated\n\nif sys.version_info >= (3, 14):\n    from _typeshed import EvaluateFunc\n    from annotationlib import Format\n\nif sys.version_info >= (3, 10):\n    from types import UnionType\n\n__all__ = [\n    'AbstractSet',\n    'Annotated',\n    'Any',\n    'AnyStr',\n    'AsyncContextManager',\n    'AsyncGenerator',\n    'AsyncIterable',\n    'AsyncIterator',\n    'Awaitable',\n    'BinaryIO',\n    'ByteString',\n    'Callable',\n    'ChainMap',\n    'ClassVar',\n    'Collection',\n    'Container',\n    'ContextManager',\n    'Coroutine',\n    'Counter',\n    'DefaultDict',\n    'Deque',\n    'Dict',\n    'Final',\n    'ForwardRef',\n    'FrozenSet',\n    'Generator',\n    'Generic',\n    'Hashable',\n    'IO',\n    'ItemsView',\n    'Iterable',\n    'Iterator',\n    'KeysView',\n    'List',\n    'Literal',\n    'Mapping',\n    'MappingView',\n    'Match',\n    'MutableMapping',\n    'MutableSequence',\n    'MutableSet',\n    'NamedTuple',\n    'NewType',\n    'NoReturn',\n    'Optional',\n    'OrderedDict',\n    'Pattern',\n    'Protocol',\n    'Reversible',\n    'Sequence',\n    'Set',\n    'Sized',\n    'SupportsAbs',\n    'SupportsBytes',\n    'SupportsComplex',\n    'SupportsFloat',\n    'SupportsIndex',\n    'SupportsInt',\n    'SupportsRound',\n    'Text',\n    'TextIO',\n    'Tuple',\n    'Type',\n    'TypeVar',\n    'TypedDict',\n    'Union',\n    'ValuesView',\n    'TYPE_CHECKING',\n    'cast',\n    'final',\n    'get_args',\n    'get_origin',\n    'get_type_hints',\n    'no_type_check',\n    'no_type_check_decorator',\n    'overload',\n    'runtime_checkable',\n]\n\nif sys.version_info >= (3, 14):\n    __all__ += ['evaluate_forward_ref']\n\nif sys.version_info >= (3, 10):\n    __all__ += [\n        'Concatenate',\n        'ParamSpec',\n        'ParamSpecArgs',\n        'ParamSpecKwargs',\n        'TypeAlias',\n        'TypeGuard',\n        'is_typeddict',\n    ]\n\nif sys.version_info >= (3, 11):\n    __all__ += [\n        'LiteralString',\n        'Never',\n        'NotRequired',\n        'Required',\n        'Self',\n        'TypeVarTuple',\n        'Unpack',\n        'assert_never',\n        'assert_type',\n        'clear_overloads',\n        'dataclass_transform',\n        'get_overloads',\n        'reveal_type',\n    ]\n\nif sys.version_info >= (3, 12):\n    __all__ += ['TypeAliasType', 'override']\n\nif sys.version_info >= (3, 13):\n    __all__ += ['get_protocol_members', 'is_protocol', 'NoDefault', 'TypeIs', 'ReadOnly']\n\n# We can't use this name here because it leads to issues with mypy, likely\n# due to an import cycle. Below instead we use Any with a comment.\n# from _typeshed import AnnotationForm\n\nclass Any: ...\n\nclass _Final:\n    __slots__ = ('__weakref__',)\n\ndef final(f: _T) -> _T: ...\n@final\nclass TypeVar:\n    @property\n    def __name__(self) -> str: ...\n    @property\n    def __bound__(self) -> Any | None: ...  # AnnotationForm\n    @property\n    def __constraints__(self) -> tuple[Any, ...]: ...  # AnnotationForm\n    @property\n    def __covariant__(self) -> bool: ...\n    @property\n    def __contravariant__(self) -> bool: ...\n    if sys.version_info >= (3, 12):\n        @property\n        def __infer_variance__(self) -> bool: ...\n    if sys.version_info >= (3, 13):\n        @property\n        def __default__(self) -> Any: ...  # AnnotationForm\n    if sys.version_info >= (3, 13):\n        def __new__(\n            cls,\n            name: str,\n            *constraints: Any,  # AnnotationForm\n            bound: Any | None = None,  # AnnotationForm\n            contravariant: bool = False,\n            covariant: bool = False,\n            infer_variance: bool = False,\n            default: Any = ...,  # AnnotationForm\n        ) -> Self: ...\n    elif sys.version_info >= (3, 12):\n        def __new__(\n            cls,\n            name: str,\n            *constraints: Any,  # AnnotationForm\n            bound: Any | None = None,  # AnnotationForm\n            covariant: bool = False,\n            contravariant: bool = False,\n            infer_variance: bool = False,\n        ) -> Self: ...\n    elif sys.version_info >= (3, 11):\n        def __new__(\n            cls,\n            name: str,\n            *constraints: Any,  # AnnotationForm\n            bound: Any | None = None,  # AnnotationForm\n            covariant: bool = False,\n            contravariant: bool = False,\n        ) -> Self: ...\n    else:\n        def __init__(\n            self,\n            name: str,\n            *constraints: Any,  # AnnotationForm\n            bound: Any | None = None,  # AnnotationForm\n            covariant: bool = False,\n            contravariant: bool = False,\n        ) -> None: ...\n    if sys.version_info >= (3, 10):\n        def __or__(self, right: Any, /) -> _SpecialForm: ...  # AnnotationForm\n        def __ror__(self, left: Any, /) -> _SpecialForm: ...  # AnnotationForm\n    if sys.version_info >= (3, 11):\n        def __typing_subst__(self, arg: Any, /) -> Any: ...\n    if sys.version_info >= (3, 13):\n        def __typing_prepare_subst__(self, alias: Any, args: Any, /) -> tuple[Any, ...]: ...\n        def has_default(self) -> bool: ...\n    if sys.version_info >= (3, 14):\n        @property\n        def evaluate_bound(self) -> EvaluateFunc | None: ...\n        @property\n        def evaluate_constraints(self) -> EvaluateFunc | None: ...\n        @property\n        def evaluate_default(self) -> EvaluateFunc | None: ...\n\n# N.B. Keep this definition in sync with typing_extensions._SpecialForm\n@final\nclass _SpecialForm(_Final):\n    __slots__ = ('_name', '__doc__', '_getitem')\n    def __getitem__(self, parameters: Any) -> object: ...\n    if sys.version_info >= (3, 10):\n        def __or__(self, other: Any) -> _SpecialForm: ...\n        def __ror__(self, other: Any) -> _SpecialForm: ...\n\nUnion: _SpecialForm\nProtocol: _SpecialForm\nCallable: _SpecialForm\nType: _SpecialForm\nNoReturn: _SpecialForm\nClassVar: _SpecialForm\n\nOptional: _SpecialForm\nTuple: _SpecialForm\nFinal: _SpecialForm\n\nLiteral: _SpecialForm\nTypedDict: _SpecialForm\n\nif sys.version_info >= (3, 11):\n    Self: _SpecialForm\n    Never: _SpecialForm\n    Unpack: _SpecialForm\n    Required: _SpecialForm\n    NotRequired: _SpecialForm\n    LiteralString: _SpecialForm\n\n    @final\n    class TypeVarTuple:\n        @property\n        def __name__(self) -> str: ...\n        if sys.version_info >= (3, 13):\n            @property\n            def __default__(self) -> Any: ...  # AnnotationForm\n            def has_default(self) -> bool: ...\n        if sys.version_info >= (3, 13):\n            def __new__(cls, name: str, *, default: Any = ...) -> Self: ...  # AnnotationForm\n        elif sys.version_info >= (3, 12):\n            def __new__(cls, name: str) -> Self: ...\n        else:\n            def __init__(self, name: str) -> None: ...\n\n        def __iter__(self) -> Any: ...\n        def __typing_subst__(self, arg: Never, /) -> Never: ...\n        def __typing_prepare_subst__(self, alias: Any, args: Any, /) -> tuple[Any, ...]: ...\n        if sys.version_info >= (3, 14):\n            @property\n            def evaluate_default(self) -> EvaluateFunc | None: ...\n\nif sys.version_info >= (3, 10):\n    @final\n    class ParamSpecArgs:\n        @property\n        def __origin__(self) -> ParamSpec: ...\n        if sys.version_info >= (3, 12):\n            def __new__(cls, origin: ParamSpec) -> Self: ...\n        else:\n            def __init__(self, origin: ParamSpec) -> None: ...\n\n        def __eq__(self, other: object, /) -> bool: ...\n        __hash__: ClassVar[None]  # type: ignore[assignment]\n\n    @final\n    class ParamSpecKwargs:\n        @property\n        def __origin__(self) -> ParamSpec: ...\n        if sys.version_info >= (3, 12):\n            def __new__(cls, origin: ParamSpec) -> Self: ...\n        else:\n            def __init__(self, origin: ParamSpec) -> None: ...\n\n        def __eq__(self, other: object, /) -> bool: ...\n        __hash__: ClassVar[None]  # type: ignore[assignment]\n\n    @final\n    class ParamSpec:\n        @property\n        def __name__(self) -> str: ...\n        @property\n        def __bound__(self) -> Any | None: ...  # AnnotationForm\n        @property\n        def __covariant__(self) -> bool: ...\n        @property\n        def __contravariant__(self) -> bool: ...\n        if sys.version_info >= (3, 12):\n            @property\n            def __infer_variance__(self) -> bool: ...\n        if sys.version_info >= (3, 13):\n            @property\n            def __default__(self) -> Any: ...  # AnnotationForm\n        if sys.version_info >= (3, 13):\n            def __new__(\n                cls,\n                name: str,\n                *,\n                bound: Any | None = None,  # AnnotationForm\n                contravariant: bool = False,\n                covariant: bool = False,\n                infer_variance: bool = False,\n                default: Any = ...,  # AnnotationForm\n            ) -> Self: ...\n        elif sys.version_info >= (3, 12):\n            def __new__(\n                cls,\n                name: str,\n                *,\n                bound: Any | None = None,  # AnnotationForm\n                contravariant: bool = False,\n                covariant: bool = False,\n                infer_variance: bool = False,\n            ) -> Self: ...\n        elif sys.version_info >= (3, 11):\n            def __new__(\n                cls,\n                name: str,\n                *,\n                bound: Any | None = None,  # AnnotationForm\n                contravariant: bool = False,\n                covariant: bool = False,\n            ) -> Self: ...\n        else:\n            def __init__(\n                self,\n                name: str,\n                *,\n                bound: Any | None = None,  # AnnotationForm\n                contravariant: bool = False,\n                covariant: bool = False,\n            ) -> None: ...\n\n        @property\n        def args(self) -> ParamSpecArgs: ...\n        @property\n        def kwargs(self) -> ParamSpecKwargs: ...\n        if sys.version_info >= (3, 11):\n            def __typing_subst__(self, arg: Any, /) -> Any: ...\n            def __typing_prepare_subst__(self, alias: Any, args: Any, /) -> tuple[Any, ...]: ...\n\n        def __or__(self, right: Any, /) -> _SpecialForm: ...\n        def __ror__(self, left: Any, /) -> _SpecialForm: ...\n        if sys.version_info >= (3, 13):\n            def has_default(self) -> bool: ...\n        if sys.version_info >= (3, 14):\n            @property\n            def evaluate_default(self) -> EvaluateFunc | None: ...\n\n    Concatenate: _SpecialForm\n    TypeAlias: _SpecialForm\n    TypeGuard: _SpecialForm\n\n    class NewType:\n        def __init__(self, name: str, tp: Any) -> None: ...  # AnnotationForm\n        if sys.version_info >= (3, 11):\n            @staticmethod\n            def __call__(x: _T, /) -> _T: ...\n        else:\n            def __call__(self, x: _T) -> _T: ...\n\n        def __or__(self, other: Any) -> _SpecialForm: ...\n        def __ror__(self, other: Any) -> _SpecialForm: ...\n        __supertype__: type | NewType\n        __name__: str\n\nelse:\n    def NewType(name: str, tp: Any) -> Any: ...\n\n_F = TypeVar('_F', bound=Callable[..., Any])\n_P = _ParamSpec('_P')\n_T = TypeVar('_T')\n\n_FT = TypeVar('_FT', bound=Callable[..., Any] | type)\n\n# These type variables are used by the container types.\n_S = TypeVar('_S')\n_KT = TypeVar('_KT')  # Key type.\n_VT = TypeVar('_VT')  # Value type.\n_T_co = TypeVar('_T_co', covariant=True)  # Any type covariant containers.\n_KT_co = TypeVar('_KT_co', covariant=True)  # Key type covariant containers.\n_VT_co = TypeVar('_VT_co', covariant=True)  # Value type covariant containers.\n_TC = TypeVar('_TC', bound=type[object])\n\ndef overload(func: _F) -> _F: ...\ndef no_type_check(arg: _F) -> _F: ...\n\nif sys.version_info >= (3, 13):\n    @deprecated('Deprecated since Python 3.13; removed in Python 3.15.')\n    def no_type_check_decorator(decorator: Callable[_P, _T]) -> Callable[_P, _T]: ...\n\nelse:\n    def no_type_check_decorator(decorator: Callable[_P, _T]) -> Callable[_P, _T]: ...\n\n# This itself is only available during type checking\ndef type_check_only(func_or_cls: _FT) -> _FT: ...\n\n# Type aliases and type constructors\n\n@type_check_only\nclass _Alias:\n    # Class for defining generic aliases for library types.\n    def __getitem__(self, typeargs: Any) -> Any: ...\n\nList = _Alias()\nDict = _Alias()\nDefaultDict = _Alias()\nSet = _Alias()\nFrozenSet = _Alias()\nCounter = _Alias()\nDeque = _Alias()\nChainMap = _Alias()\n\nOrderedDict = _Alias()\n\nAnnotated: _SpecialForm\n\n# Predefined type variables.\nAnyStr = TypeVar('AnyStr', str, bytes)\n\n@type_check_only\nclass _Generic:\n    if sys.version_info < (3, 12):\n        __slots__ = ()\n\n    if sys.version_info >= (3, 10):\n        @classmethod\n        def __class_getitem__(cls, args: TypeVar | ParamSpec | tuple[TypeVar | ParamSpec, ...]) -> _Final: ...\n    else:\n        @classmethod\n        def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...\n\nGeneric: type[_Generic]\n\nclass _ProtocolMeta(ABCMeta):\n    if sys.version_info >= (3, 12):\n        def __init__(cls, *args: Any, **kwargs: Any) -> None: ...\n\n# Abstract base classes.\n\ndef runtime_checkable(cls: _TC) -> _TC: ...\n@runtime_checkable\nclass SupportsInt(Protocol, metaclass=ABCMeta):\n    __slots__ = ()\n    @abstractmethod\n    def __int__(self) -> int: ...\n\n@runtime_checkable\nclass SupportsFloat(Protocol, metaclass=ABCMeta):\n    __slots__ = ()\n    @abstractmethod\n    def __float__(self) -> float: ...\n\n@runtime_checkable\nclass SupportsComplex(Protocol, metaclass=ABCMeta):\n    __slots__ = ()\n    @abstractmethod\n    def __complex__(self) -> complex: ...\n\n@runtime_checkable\nclass SupportsBytes(Protocol, metaclass=ABCMeta):\n    __slots__ = ()\n    @abstractmethod\n    def __bytes__(self) -> bytes: ...\n\n@runtime_checkable\nclass SupportsIndex(Protocol, metaclass=ABCMeta):\n    __slots__ = ()\n    @abstractmethod\n    def __index__(self) -> int: ...\n\n@runtime_checkable\nclass SupportsAbs(Protocol[_T_co]):\n    __slots__ = ()\n    @abstractmethod\n    def __abs__(self) -> _T_co: ...\n\n@runtime_checkable\nclass SupportsRound(Protocol[_T_co]):\n    __slots__ = ()\n    @overload\n    @abstractmethod\n    def __round__(self) -> int: ...\n    @overload\n    @abstractmethod\n    def __round__(self, ndigits: int, /) -> _T_co: ...\n\n@runtime_checkable\nclass Sized(Protocol, metaclass=ABCMeta):\n    @abstractmethod\n    def __len__(self) -> int: ...\n\n@runtime_checkable\nclass Hashable(Protocol, metaclass=ABCMeta):\n    # TODO: This is special, in that a subclass of a hashable class may not be hashable\n    #   (for example, list vs. object). It's not obvious how to represent this. This class\n    #   is currently mostly useless for static checking.\n    @abstractmethod\n    def __hash__(self) -> int: ...\n\n@runtime_checkable\nclass Iterable(Protocol[_T_co]):\n    @abstractmethod\n    def __iter__(self) -> Iterator[_T_co]: ...\n\n@runtime_checkable\nclass Iterator(Iterable[_T_co], Protocol[_T_co]):\n    @abstractmethod\n    def __next__(self) -> _T_co: ...\n    def __iter__(self) -> Iterator[_T_co]: ...\n\n@runtime_checkable\nclass Reversible(Iterable[_T_co], Protocol[_T_co]):\n    @abstractmethod\n    def __reversed__(self) -> Iterator[_T_co]: ...\n\n_YieldT_co = TypeVar('_YieldT_co', covariant=True)\n_SendT_contra = TypeVar('_SendT_contra', contravariant=True, default=None)\n_ReturnT_co = TypeVar('_ReturnT_co', covariant=True, default=None)\n\n@runtime_checkable\nclass Generator(Iterator[_YieldT_co], Protocol[_YieldT_co, _SendT_contra, _ReturnT_co]):\n    def __next__(self) -> _YieldT_co: ...\n    @abstractmethod\n    def send(self, value: _SendT_contra, /) -> _YieldT_co: ...\n    @overload\n    @abstractmethod\n    def throw(\n        self, typ: type[BaseException], val: BaseException | object = None, tb: TracebackType | None = None, /\n    ) -> _YieldT_co: ...\n    @overload\n    @abstractmethod\n    def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = None, /) -> _YieldT_co: ...\n    if sys.version_info >= (3, 13):\n        def close(self) -> _ReturnT_co | None: ...\n    else:\n        def close(self) -> None: ...\n\n    def __iter__(self) -> Generator[_YieldT_co, _SendT_contra, _ReturnT_co]: ...\n\n# NOTE: Prior to Python 3.13 these aliases are lacking the second _ExitT_co parameter\nif sys.version_info >= (3, 13):\n    from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager\nelse:\n    from contextlib import AbstractAsyncContextManager, AbstractContextManager\n\n    @runtime_checkable\n    class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ...\n\n    @runtime_checkable\n    class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ...\n\n@runtime_checkable\nclass Awaitable(Protocol[_T_co]):\n    @abstractmethod\n    def __await__(self) -> Generator[Any, Any, _T_co]: ...\n\n# Non-default variations to accommodate coroutines, and `AwaitableGenerator` having a 4th type parameter.\n_SendT_nd_contra = TypeVar('_SendT_nd_contra', contravariant=True)\n_ReturnT_nd_co = TypeVar('_ReturnT_nd_co', covariant=True)\n\nclass Coroutine(Awaitable[_ReturnT_nd_co], Generic[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co]):\n    __name__: str\n    __qualname__: str\n\n    @abstractmethod\n    def send(self, value: _SendT_nd_contra, /) -> _YieldT_co: ...\n    @overload\n    @abstractmethod\n    def throw(\n        self, typ: type[BaseException], val: BaseException | object = None, tb: TracebackType | None = None, /\n    ) -> _YieldT_co: ...\n    @overload\n    @abstractmethod\n    def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = None, /) -> _YieldT_co: ...\n    @abstractmethod\n    def close(self) -> None: ...\n\n# NOTE: This type does not exist in typing.py or PEP 484 but mypy needs it to exist.\n# The parameters correspond to Generator, but the 4th is the original type.\n# Obsolete, use _typeshed._type_checker_internals.AwaitableGenerator instead.\n@type_check_only\nclass AwaitableGenerator(\n    Awaitable[_ReturnT_nd_co],\n    Generator[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co],\n    Generic[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co, _S],\n    metaclass=ABCMeta,\n): ...\n\n@runtime_checkable\nclass AsyncIterable(Protocol[_T_co]):\n    @abstractmethod\n    def __aiter__(self) -> AsyncIterator[_T_co]: ...\n\n@runtime_checkable\nclass AsyncIterator(AsyncIterable[_T_co], Protocol[_T_co]):\n    @abstractmethod\n    def __anext__(self) -> Awaitable[_T_co]: ...\n    def __aiter__(self) -> AsyncIterator[_T_co]: ...\n\n@runtime_checkable\nclass AsyncGenerator(AsyncIterator[_YieldT_co], Protocol[_YieldT_co, _SendT_contra]):\n    def __anext__(self) -> Coroutine[Any, Any, _YieldT_co]: ...\n    @abstractmethod\n    def asend(self, value: _SendT_contra, /) -> Coroutine[Any, Any, _YieldT_co]: ...\n    @overload\n    @abstractmethod\n    def athrow(\n        self, typ: type[BaseException], val: BaseException | object = None, tb: TracebackType | None = None, /\n    ) -> Coroutine[Any, Any, _YieldT_co]: ...\n    @overload\n    @abstractmethod\n    def athrow(\n        self, typ: BaseException, val: None = None, tb: TracebackType | None = None, /\n    ) -> Coroutine[Any, Any, _YieldT_co]: ...\n    def aclose(self) -> Coroutine[Any, Any, None]: ...\n\n_ContainerT_contra = TypeVar('_ContainerT_contra', contravariant=True, default=Any)\n\n@runtime_checkable\nclass Container(Protocol[_ContainerT_contra]):\n    # This is generic more on vibes than anything else\n    @abstractmethod\n    def __contains__(self, x: _ContainerT_contra, /) -> bool: ...\n\n@runtime_checkable\nclass Collection(Iterable[_T_co], Container[Any], Protocol[_T_co]):\n    # Note: need to use Container[Any] instead of Container[_T_co] to ensure covariance.\n    # Implement Sized (but don't have it as a base class).\n    @abstractmethod\n    def __len__(self) -> int: ...\n\nclass Sequence(Reversible[_T_co], Collection[_T_co]):\n    @overload\n    @abstractmethod\n    def __getitem__(self, index: int, /) -> _T_co: ...\n    @overload\n    @abstractmethod\n    def __getitem__(self, index: slice[int | None], /) -> Sequence[_T_co]: ...\n    # Mixin methods\n    def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...\n    def count(self, value: Any, /) -> int: ...\n    def __contains__(self, value: object, /) -> bool: ...\n    def __iter__(self) -> Iterator[_T_co]: ...\n    def __reversed__(self) -> Iterator[_T_co]: ...\n\nclass MutableSequence(Sequence[_T]):\n    @abstractmethod\n    def insert(self, index: int, value: _T, /) -> None: ...\n    @overload\n    @abstractmethod\n    def __getitem__(self, index: int, /) -> _T: ...\n    @overload\n    @abstractmethod\n    def __getitem__(self, index: slice[int | None], /) -> MutableSequence[_T]: ...\n    @overload\n    @abstractmethod\n    def __setitem__(self, index: int, value: _T, /) -> None: ...\n    @overload\n    @abstractmethod\n    def __setitem__(self, index: slice[int | None], value: Iterable[_T], /) -> None: ...\n    @overload\n    @abstractmethod\n    def __delitem__(self, index: int, /) -> None: ...\n    @overload\n    @abstractmethod\n    def __delitem__(self, index: slice[int | None], /) -> None: ...\n    # Mixin methods\n    def append(self, value: _T, /) -> None: ...\n    def clear(self) -> None: ...\n    def extend(self, values: Iterable[_T], /) -> None: ...\n    def reverse(self) -> None: ...\n    def pop(self, index: int = -1, /) -> _T: ...\n    def remove(self, value: _T, /) -> None: ...\n    def __iadd__(self, values: Iterable[_T], /) -> typing_extensions.Self: ...\n\nclass AbstractSet(Collection[_T_co]):\n    @abstractmethod\n    def __contains__(self, x: object, /) -> bool: ...\n    def _hash(self) -> int: ...\n    # Mixin methods\n    @classmethod\n    def _from_iterable(cls, it: Iterable[_S], /) -> AbstractSet[_S]: ...\n    def __le__(self, other: AbstractSet[Any], /) -> bool: ...\n    def __lt__(self, other: AbstractSet[Any], /) -> bool: ...\n    def __gt__(self, other: AbstractSet[Any], /) -> bool: ...\n    def __ge__(self, other: AbstractSet[Any], /) -> bool: ...\n    def __and__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ...\n    def __or__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ...\n    def __sub__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ...\n    def __xor__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ...\n    def __eq__(self, other: object, /) -> bool: ...\n    def isdisjoint(self, other: Iterable[Any], /) -> bool: ...\n\nclass MutableSet(AbstractSet[_T]):\n    @abstractmethod\n    def add(self, value: _T, /) -> None: ...\n    @abstractmethod\n    def discard(self, value: _T, /) -> None: ...\n    # Mixin methods\n    def clear(self) -> None: ...\n    def pop(self) -> _T: ...\n    def remove(self, value: _T, /) -> None: ...\n    def __ior__(self, it: AbstractSet[_T], /) -> typing_extensions.Self: ...  # type: ignore[override,misc]\n    def __iand__(self, it: AbstractSet[Any], /) -> typing_extensions.Self: ...\n    def __ixor__(self, it: AbstractSet[_T], /) -> typing_extensions.Self: ...  # type: ignore[override,misc]\n    def __isub__(self, it: AbstractSet[Any], /) -> typing_extensions.Self: ...\n\nclass MappingView(Sized):\n    __slots__ = ('_mapping',)\n    def __init__(self, mapping: Sized) -> None: ...  # undocumented\n    def __len__(self) -> int: ...\n\nclass ItemsView(MappingView, AbstractSet[tuple[_KT_co, _VT_co]], Generic[_KT_co, _VT_co]):\n    def __init__(self, mapping: SupportsGetItemViewable[_KT_co, _VT_co]) -> None: ...  # undocumented\n    @classmethod\n    def _from_iterable(cls, it: Iterable[_S], /) -> set[_S]: ...\n    def __and__(self, other: Iterable[Any], /) -> set[tuple[_KT_co, _VT_co]]: ...\n    def __rand__(self, other: Iterable[_T], /) -> set[_T]: ...\n    def __contains__(self, item: tuple[object, object], /) -> bool: ...  # type: ignore[override]\n    def __iter__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...\n    def __or__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ...\n    def __ror__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ...\n    def __sub__(self, other: Iterable[Any], /) -> set[tuple[_KT_co, _VT_co]]: ...\n    def __rsub__(self, other: Iterable[_T], /) -> set[_T]: ...\n    def __xor__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ...\n    def __rxor__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ...\n\nclass KeysView(MappingView, AbstractSet[_KT_co]):\n    def __init__(self, mapping: Viewable[_KT_co]) -> None: ...  # undocumented\n    @classmethod\n    def _from_iterable(cls, it: Iterable[_S], /) -> set[_S]: ...\n    def __and__(self, other: Iterable[Any], /) -> set[_KT_co]: ...\n    def __rand__(self, other: Iterable[_T], /) -> set[_T]: ...\n    def __contains__(self, key: object, /) -> bool: ...\n    def __iter__(self) -> Iterator[_KT_co]: ...\n    def __or__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ...\n    def __ror__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ...\n    def __sub__(self, other: Iterable[Any], /) -> set[_KT_co]: ...\n    def __rsub__(self, other: Iterable[_T], /) -> set[_T]: ...\n    def __xor__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ...\n    def __rxor__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ...\n\nclass ValuesView(MappingView, Collection[_VT_co]):\n    def __init__(self, mapping: SupportsGetItemViewable[Any, _VT_co]) -> None: ...  # undocumented\n    def __contains__(self, value: object, /) -> bool: ...\n    def __iter__(self) -> Iterator[_VT_co]: ...\n\n# note for Mapping.get and MutableMapping.pop and MutableMapping.setdefault\n# In _collections_abc.py the parameters are positional-or-keyword,\n# but dict and types.MappingProxyType (the vast majority of Mapping types)\n# don't allow keyword arguments.\n\nclass Mapping(Collection[_KT], Generic[_KT, _VT_co]):\n    # TODO: We wish the key type could also be covariant, but that doesn't work,\n    # see discussion in https://github.com/python/typing/pull/273.\n    @abstractmethod\n    def __getitem__(self, key: _KT, /) -> _VT_co: ...\n    # Mixin methods\n    @overload\n    def get(self, key: _KT, /) -> _VT_co | None: ...\n    @overload\n    def get(self, key: _KT, default: _VT_co, /) -> _VT_co: ...  # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter\n    @overload\n    def get(self, key: _KT, default: _T, /) -> _VT_co | _T: ...\n    def items(self) -> ItemsView[_KT, _VT_co]: ...\n    def keys(self) -> KeysView[_KT]: ...\n    def values(self) -> ValuesView[_VT_co]: ...\n    def __contains__(self, key: object, /) -> bool: ...\n    def __eq__(self, other: object, /) -> bool: ...\n\nclass MutableMapping(Mapping[_KT, _VT]):\n    @abstractmethod\n    def __setitem__(self, key: _KT, value: _VT, /) -> None: ...\n    @abstractmethod\n    def __delitem__(self, key: _KT, /) -> None: ...\n    def clear(self) -> None: ...\n    @overload\n    def pop(self, key: _KT, /) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _VT, /) -> _VT: ...\n    @overload\n    def pop(self, key: _KT, default: _T, /) -> _VT | _T: ...\n    def popitem(self) -> tuple[_KT, _VT]: ...\n    # This overload should be allowed only if the value type is compatible with None.\n    #\n    # Keep the following methods in line with MutableMapping.setdefault, modulo positional-only differences:\n    # -- collections.OrderedDict.setdefault\n    # -- collections.ChainMap.setdefault\n    # -- weakref.WeakKeyDictionary.setdefault\n    @overload\n    def setdefault(self: MutableMapping[_KT, _T | None], key: _KT, default: None = None, /) -> _T | None: ...\n    @overload\n    def setdefault(self, key: _KT, default: _VT, /) -> _VT: ...\n    # 'update' used to take a Union, but using overloading is better.\n    # The second overloaded type here is a bit too general, because\n    # Mapping[tuple[_KT, _VT], W] is a subclass of Iterable[tuple[_KT, _VT]],\n    # but will always have the behavior of the first overloaded type\n    # at runtime, leading to keys of a mix of types _KT and tuple[_KT, _VT].\n    # We don't currently have any way of forcing all Mappings to use\n    # the first overload, but by using overloading rather than a Union,\n    # mypy will commit to using the first overload when the argument is\n    # known to be a Mapping with unknown type parameters, which is closer\n    # to the behavior we want. See mypy issue  #1430.\n    #\n    # Various mapping classes have __ior__ methods that should be kept roughly in line with .update():\n    # -- dict.__ior__\n    # -- os._Environ.__ior__\n    # -- collections.UserDict.__ior__\n    # -- collections.ChainMap.__ior__\n    # -- peewee.attrdict.__add__\n    # -- peewee.attrdict.__iadd__\n    # -- weakref.WeakValueDictionary.__ior__\n    # -- weakref.WeakKeyDictionary.__ior__\n    @overload\n    def update(self, m: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ...\n    @overload\n    def update(self: SupportsGetItem[str, _VT], m: SupportsKeysAndGetItem[str, _VT], /, **kwargs: _VT) -> None: ...\n    @overload\n    def update(self, m: Iterable[tuple[_KT, _VT]], /) -> None: ...\n    @overload\n    def update(self: SupportsGetItem[str, _VT], m: Iterable[tuple[str, _VT]], /, **kwargs: _VT) -> None: ...\n    @overload\n    def update(self: SupportsGetItem[str, _VT], /, **kwargs: _VT) -> None: ...\n\nText = str\n\nTYPE_CHECKING: Final[bool]\n\n# In stubs, the arguments of the IO class are marked as positional-only.\n# This differs from runtime, but better reflects the fact that in reality\n# classes deriving from IO use different names for the arguments.\nclass IO(Generic[AnyStr]):\n    # At runtime these are all abstract properties,\n    # but making them abstract in the stub is hugely disruptive, for not much gain.\n    # See #8726\n    __slots__ = ()\n    @property\n    def mode(self) -> str: ...\n    # Usually str, but may be bytes if a bytes path was passed to open(). See #10737.\n    # If PEP 696 becomes available, we may want to use a defaulted TypeVar here.\n    @property\n    def name(self) -> str | Any: ...\n    @abstractmethod\n    def close(self) -> None: ...\n    @property\n    def closed(self) -> bool: ...\n    @abstractmethod\n    def fileno(self) -> int: ...\n    @abstractmethod\n    def flush(self) -> None: ...\n    @abstractmethod\n    def isatty(self) -> bool: ...\n    @abstractmethod\n    def read(self, n: int = -1, /) -> AnyStr: ...\n    @abstractmethod\n    def readable(self) -> bool: ...\n    @abstractmethod\n    def readline(self, limit: int = -1, /) -> AnyStr: ...\n    @abstractmethod\n    def readlines(self, hint: int = -1, /) -> list[AnyStr]: ...\n    @abstractmethod\n    def seek(self, offset: int, whence: int = 0, /) -> int: ...\n    @abstractmethod\n    def seekable(self) -> bool: ...\n    @abstractmethod\n    def tell(self) -> int: ...\n    @abstractmethod\n    def truncate(self, size: int | None = None, /) -> int: ...\n    @abstractmethod\n    def writable(self) -> bool: ...\n    @abstractmethod\n    @overload\n    def write(self: IO[bytes], s: ReadableBuffer, /) -> int: ...\n    @abstractmethod\n    @overload\n    def write(self, s: AnyStr, /) -> int: ...\n    @abstractmethod\n    @overload\n    def writelines(self: IO[bytes], lines: Iterable[ReadableBuffer], /) -> None: ...\n    @abstractmethod\n    @overload\n    def writelines(self, lines: Iterable[AnyStr], /) -> None: ...\n    @abstractmethod\n    def __next__(self) -> AnyStr: ...\n    @abstractmethod\n    def __iter__(self) -> Iterator[AnyStr]: ...\n    @abstractmethod\n    def __enter__(self) -> IO[AnyStr]: ...\n    @abstractmethod\n    def __exit__(\n        self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None, /\n    ) -> None: ...\n\nclass BinaryIO(IO[bytes]):\n    __slots__ = ()\n    @abstractmethod\n    def __enter__(self) -> BinaryIO: ...\n\nclass TextIO(IO[str]):\n    # See comment regarding the @properties in the `IO` class\n    __slots__ = ()\n    @property\n    def buffer(self) -> BinaryIO: ...\n    @property\n    def encoding(self) -> str: ...\n    @property\n    def errors(self) -> str | None: ...\n    @property\n    def line_buffering(self) -> int: ...  # int on PyPy, bool on CPython\n    @property\n    def newlines(self) -> Any: ...  # None, str or tuple\n    @abstractmethod\n    def __enter__(self) -> TextIO: ...\n\nByteString: typing_extensions.TypeAlias = bytes | bytearray | memoryview\n\n# Functions\n\n_get_type_hints_obj_allowed_types: typing_extensions.TypeAlias = (\n    object\n    | Callable[..., Any]\n    | FunctionType\n    | BuiltinFunctionType\n    | MethodType\n    | ModuleType\n    | WrapperDescriptorType\n    | MethodWrapperType\n    | MethodDescriptorType\n)\n\nif sys.version_info >= (3, 14):\n    def get_type_hints(\n        obj: _get_type_hints_obj_allowed_types,\n        globalns: dict[str, Any] | None = None,\n        localns: Mapping[str, Any] | None = None,\n        include_extras: bool = False,\n        *,\n        format: Format | None = None,  # Default: Format.VALUE\n    ) -> dict[str, Any]: ...  # AnnotationForm\n\nelse:\n    def get_type_hints(\n        obj: _get_type_hints_obj_allowed_types,\n        globalns: dict[str, Any] | None = None,\n        localns: Mapping[str, Any] | None = None,\n        include_extras: bool = False,\n    ) -> dict[str, Any]: ...  # AnnotationForm\n\ndef get_args(tp: Any) -> tuple[Any, ...]: ...  # AnnotationForm\n\nif sys.version_info >= (3, 10):\n    @overload\n    def get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ...\n    @overload\n    def get_origin(tp: UnionType) -> type[UnionType]: ...\n\n@overload\ndef get_origin(tp: GenericAlias) -> type: ...\n@overload\ndef get_origin(tp: Any) -> Any | None: ...  # AnnotationForm\n@overload\ndef cast(typ: type[_T], val: Any) -> _T: ...\n@overload\ndef cast(typ: str, val: Any) -> Any: ...\n@overload\ndef cast(typ: object, val: Any) -> Any: ...\n\nif sys.version_info >= (3, 11):\n    def reveal_type(obj: _T, /) -> _T: ...\n    def assert_never(arg: Never, /) -> Never: ...\n    def assert_type(val: _T, typ: Any, /) -> _T: ...  # AnnotationForm\n    def clear_overloads() -> None: ...\n    def get_overloads(func: Callable[..., object]) -> Sequence[Callable[..., object]]: ...\n    def dataclass_transform(\n        *,\n        eq_default: bool = True,\n        order_default: bool = False,\n        kw_only_default: bool = False,\n        frozen_default: bool = False,  # on 3.11, runtime accepts it as part of kwargs\n        field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (),\n        **kwargs: Any,\n    ) -> IdentityFunction: ...\n\n# Type constructors\n\n# Obsolete, will be changed to a function. Use _typeshed._type_checker_internals.NamedTupleFallback instead.\nclass NamedTuple(tuple[Any, ...]):\n    _field_defaults: ClassVar[dict[str, Any]]\n    _fields: ClassVar[tuple[str, ...]]\n    # __orig_bases__ sometimes exists on <3.12, but not consistently\n    # So we only add it to the stub on 3.12+.\n    if sys.version_info >= (3, 12):\n        __orig_bases__: ClassVar[tuple[Any, ...]]\n\n    @overload\n    def __init__(self, typename: str, fields: Iterable[tuple[str, Any]], /) -> None: ...\n    @overload\n    @deprecated(\n        'Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15'\n    )\n    def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ...\n    @classmethod\n    def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ...\n    def _asdict(self) -> dict[str, Any]: ...\n    def _replace(self, **kwargs: Any) -> typing_extensions.Self: ...\n    if sys.version_info >= (3, 13):\n        def __replace__(self, **kwargs: Any) -> typing_extensions.Self: ...\n\n# Internal mypy fallback type for all typed dicts (does not exist at runtime)\n# N.B. Keep this mostly in sync with typing_extensions._TypedDict/mypy_extensions._TypedDict\n# Obsolete, use _typeshed._type_checker_internals.TypedDictFallback instead.\n@type_check_only\nclass _TypedDict(Mapping[str, object], metaclass=ABCMeta):\n    __total__: ClassVar[bool]\n    __required_keys__: ClassVar[frozenset[str]]\n    __optional_keys__: ClassVar[frozenset[str]]\n    # __orig_bases__ sometimes exists on <3.12, but not consistently,\n    # so we only add it to the stub on 3.12+\n    if sys.version_info >= (3, 12):\n        __orig_bases__: ClassVar[tuple[Any, ...]]\n    if sys.version_info >= (3, 13):\n        __readonly_keys__: ClassVar[frozenset[str]]\n        __mutable_keys__: ClassVar[frozenset[str]]\n\n    def copy(self) -> typing_extensions.Self: ...\n    # Using Never so that only calls using mypy plugin hook that specialize the signature\n    # can go through.\n    def setdefault(self, k: _Never, default: object) -> object: ...\n    # Mypy plugin hook for 'pop' expects that 'default' has a type variable type.\n    def pop(self, k: _Never, default: _T = ...) -> object: ...  # pyright: ignore[reportInvalidTypeVarUse]\n    def update(self, m: typing_extensions.Self, /) -> None: ...\n    def __delitem__(self, k: _Never) -> None: ...\n    def items(self) -> dict_items[str, object]: ...\n    def keys(self) -> dict_keys[str, object]: ...\n    def values(self) -> dict_values[str, object]: ...\n    @overload\n    def __or__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ...\n    @overload\n    def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ...\n    @overload\n    def __ror__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ...\n    @overload\n    def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ...\n    # supposedly incompatible definitions of __or__ and __ior__\n    def __ior__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ...  # type: ignore[misc]\n\nif sys.version_info >= (3, 14):\n    from annotationlib import ForwardRef as ForwardRef\n\n    def evaluate_forward_ref(\n        forward_ref: ForwardRef,\n        *,\n        owner: object = None,\n        globals: dict[str, Any] | None = None,\n        locals: Mapping[str, Any] | None = None,\n        type_params: tuple[TypeVar, ParamSpec, TypeVarTuple] | None = None,\n        format: Format | None = None,\n    ) -> Any: ...  # AnnotationForm\n\nelse:\n    @final\n    class ForwardRef(_Final):\n        __slots__ = (\n            '__forward_arg__',\n            '__forward_code__',\n            '__forward_evaluated__',\n            '__forward_value__',\n            '__forward_is_argument__',\n            '__forward_is_class__',\n            '__forward_module__',\n        )\n        __forward_arg__: str\n        __forward_code__: CodeType\n        __forward_evaluated__: bool\n        __forward_value__: Any | None  # AnnotationForm\n        __forward_is_argument__: bool\n        __forward_is_class__: bool\n        __forward_module__: Any | None\n\n        def __init__(\n            self, arg: str, is_argument: bool = True, module: Any | None = None, *, is_class: bool = False\n        ) -> None: ...\n\n        if sys.version_info >= (3, 13):\n            @overload\n            @deprecated(\n                \"Failing to pass a value to the 'type_params' parameter of ForwardRef._evaluate() is deprecated, \"\n                'as it leads to incorrect behaviour when evaluating a stringified annotation '\n                'that references a PEP 695 type parameter. It will be disallowed in Python 3.15.'\n            )\n            def _evaluate(\n                self,\n                globalns: dict[str, Any] | None,\n                localns: Mapping[str, Any] | None,\n                *,\n                recursive_guard: frozenset[str],\n            ) -> Any | None: ...  # AnnotationForm\n            @overload\n            def _evaluate(\n                self,\n                globalns: dict[str, Any] | None,\n                localns: Mapping[str, Any] | None,\n                type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...],\n                *,\n                recursive_guard: frozenset[str],\n            ) -> Any | None: ...  # AnnotationForm\n        elif sys.version_info >= (3, 12):\n            def _evaluate(\n                self,\n                globalns: dict[str, Any] | None,\n                localns: Mapping[str, Any] | None,\n                type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] | None = None,\n                *,\n                recursive_guard: frozenset[str],\n            ) -> Any | None: ...  # AnnotationForm\n        else:\n            def _evaluate(\n                self,\n                globalns: dict[str, Any] | None,\n                localns: Mapping[str, Any] | None,\n                recursive_guard: frozenset[str],\n            ) -> Any | None: ...  # AnnotationForm\n\n        def __eq__(self, other: object) -> bool: ...\n        def __hash__(self) -> int: ...\n        if sys.version_info >= (3, 11):\n            def __or__(self, other: Any) -> _SpecialForm: ...\n            def __ror__(self, other: Any) -> _SpecialForm: ...\n\nif sys.version_info >= (3, 10):\n    def is_typeddict(tp: object) -> bool: ...\n\ndef _type_repr(obj: object) -> str: ...\n\nif sys.version_info >= (3, 12):\n    _TypeParameter: typing_extensions.TypeAlias = (\n        TypeVar\n        | typing_extensions.TypeVar\n        | ParamSpec\n        | typing_extensions.ParamSpec\n        | TypeVarTuple\n        | typing_extensions.TypeVarTuple\n    )\n\n    def override(method: _F, /) -> _F: ...\n    @final\n    class TypeAliasType:\n        def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...\n        @property\n        def __value__(self) -> Any: ...  # AnnotationForm\n        @property\n        def __type_params__(self) -> tuple[_TypeParameter, ...]: ...\n        @property\n        def __parameters__(self) -> tuple[Any, ...]: ...  # AnnotationForm\n        @property\n        def __name__(self) -> str: ...\n        # It's writable on types, but not on instances of TypeAliasType.\n        @property\n        def __module__(self) -> str | None: ...  # type: ignore[override]\n        def __getitem__(self, parameters: Any, /) -> GenericAlias: ...  # AnnotationForm\n        def __or__(self, right: Any, /) -> _SpecialForm: ...\n        def __ror__(self, left: Any, /) -> _SpecialForm: ...\n        if sys.version_info >= (3, 14):\n            @property\n            def evaluate_value(self) -> EvaluateFunc: ...\n\nif sys.version_info >= (3, 13):\n    def is_protocol(tp: type, /) -> bool: ...\n    def get_protocol_members(tp: type, /) -> frozenset[str]: ...\n    @final\n    @type_check_only\n    class _NoDefaultType: ...\n\n    NoDefault: _NoDefaultType\n    TypeIs: _SpecialForm\n    ReadOnly: _SpecialForm\n"
  },
  {
    "path": "crates/monty-typeshed/vendor/typeshed/stdlib/typing_extensions.pyi",
    "content": "import abc\nimport enum\nimport sys\nfrom _collections_abc import dict_items, dict_keys, dict_values\nfrom collections.abc import (\n    AsyncGenerator as AsyncGenerator,\n    AsyncIterable as AsyncIterable,\n    AsyncIterator as AsyncIterator,\n    Awaitable as Awaitable,\n    Collection as Collection,\n    Container as Container,\n    Coroutine as Coroutine,\n    Generator as Generator,\n    Hashable as Hashable,\n    ItemsView as ItemsView,\n    Iterable as Iterable,\n    Iterator as Iterator,\n    KeysView as KeysView,\n    Mapping as Mapping,\n    MappingView as MappingView,\n    MutableMapping as MutableMapping,\n    MutableSequence as MutableSequence,\n    MutableSet as MutableSet,\n    Reversible as Reversible,\n    Sequence as Sequence,\n    Sized as Sized,\n    ValuesView as ValuesView,\n)\nfrom contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager\nfrom re import Match as Match, Pattern as Pattern\nfrom types import GenericAlias, ModuleType\nfrom typing import (  # noqa: Y022,Y037,Y038,Y039,UP035,RUF100\n    IO as IO,\n    TYPE_CHECKING as TYPE_CHECKING,\n    AbstractSet as AbstractSet,\n    Any as Any,\n    AnyStr as AnyStr,\n    BinaryIO as BinaryIO,\n    Callable as Callable,\n    ChainMap as ChainMap,\n    ClassVar as ClassVar,\n    Counter as Counter,\n    DefaultDict as DefaultDict,\n    Deque as Deque,\n    Dict as Dict,\n    ForwardRef as ForwardRef,\n    FrozenSet as FrozenSet,\n    Generic as Generic,\n    List as List,\n    NoReturn as NoReturn,\n    Optional as Optional,\n    Set as Set,\n    Text as Text,\n    TextIO as TextIO,\n    Tuple as Tuple,\n    Type as Type,\n    TypedDict as TypedDict,\n    TypeVar as _TypeVar,\n    Union as Union,\n    _Alias,\n    _SpecialForm,\n    cast as cast,\n    no_type_check as no_type_check,\n    no_type_check_decorator as no_type_check_decorator,\n    overload as overload,\n    type_check_only,\n)\n\nfrom _typeshed import AnnotationForm, IdentityFunction, Incomplete, Unused\n\nif sys.version_info >= (3, 10):\n    from types import UnionType\n\n# Please keep order the same as at runtime.\n__all__ = [\n    # Super-special typing primitives.\n    'Any',\n    'ClassVar',\n    'Concatenate',\n    'Final',\n    'LiteralString',\n    'ParamSpec',\n    'ParamSpecArgs',\n    'ParamSpecKwargs',\n    'Self',\n    'Type',\n    'TypeVar',\n    'TypeVarTuple',\n    'Unpack',\n    # ABCs (from collections.abc).\n    'Awaitable',\n    'AsyncIterator',\n    'AsyncIterable',\n    'Coroutine',\n    'AsyncGenerator',\n    'AsyncContextManager',\n    'Buffer',\n    'ChainMap',\n    # Concrete collection types.\n    'ContextManager',\n    'Counter',\n    'Deque',\n    'DefaultDict',\n    'NamedTuple',\n    'OrderedDict',\n    'TypedDict',\n    # Structural checks, a.k.a. protocols.\n    'SupportsAbs',\n    'SupportsBytes',\n    'SupportsComplex',\n    'SupportsFloat',\n    'SupportsIndex',\n    'SupportsInt',\n    'SupportsRound',\n    'Reader',\n    'Writer',\n    # One-off things.\n    'Annotated',\n    'assert_never',\n    'assert_type',\n    'clear_overloads',\n    'dataclass_transform',\n    'deprecated',\n    'disjoint_base',\n    'Doc',\n    'evaluate_forward_ref',\n    'get_overloads',\n    'final',\n    'Format',\n    'get_annotations',\n    'get_args',\n    'get_origin',\n    'get_original_bases',\n    'get_protocol_members',\n    'get_type_hints',\n    'IntVar',\n    'is_protocol',\n    'is_typeddict',\n    'Literal',\n    'NewType',\n    'overload',\n    'override',\n    'Protocol',\n    'Sentinel',\n    'reveal_type',\n    'runtime',\n    'runtime_checkable',\n    'Text',\n    'TypeAlias',\n    'TypeAliasType',\n    'TypeForm',\n    'TypeGuard',\n    'TypeIs',\n    'TYPE_CHECKING',\n    'type_repr',\n    'Never',\n    'NoReturn',\n    'ReadOnly',\n    'Required',\n    'NotRequired',\n    'NoDefault',\n    'NoExtraItems',\n    # Pure aliases, have always been in typing\n    'AbstractSet',\n    'AnyStr',\n    'BinaryIO',\n    'Callable',\n    'Collection',\n    'Container',\n    'Dict',\n    'ForwardRef',\n    'FrozenSet',\n    'Generator',\n    'Generic',\n    'Hashable',\n    'IO',\n    'ItemsView',\n    'Iterable',\n    'Iterator',\n    'KeysView',\n    'List',\n    'Mapping',\n    'MappingView',\n    'Match',\n    'MutableMapping',\n    'MutableSequence',\n    'MutableSet',\n    'Optional',\n    'Pattern',\n    'Reversible',\n    'Sequence',\n    'Set',\n    'Sized',\n    'TextIO',\n    'Tuple',\n    'Union',\n    'ValuesView',\n    'cast',\n    'no_type_check',\n    'no_type_check_decorator',\n    # Added dynamically\n    'CapsuleType',\n]\n\n_T = _TypeVar('_T')\n_F = _TypeVar('_F', bound=Callable[..., Any])\n_TC = _TypeVar('_TC', bound=type[object])\n_T_co = _TypeVar('_T_co', covariant=True)  # Any type covariant containers.\n_T_contra = _TypeVar('_T_contra', contravariant=True)\n\n# Do not import (and re-export) Protocol or runtime_checkable from\n# typing module because type checkers need to be able to distinguish\n# typing.Protocol and typing_extensions.Protocol so they can properly\n# warn users about potential runtime exceptions when using typing.Protocol\n# on older versions of Python.\nProtocol: _SpecialForm\n\ndef runtime_checkable(cls: _TC) -> _TC: ...\n\n# This alias for above is kept here for backwards compatibility.\nruntime = runtime_checkable\nFinal: _SpecialForm\n\ndef final(f: _F) -> _F: ...\ndef disjoint_base(cls: _TC) -> _TC: ...\n\nLiteral: _SpecialForm\n\ndef IntVar(name: str) -> Any: ...  # returns a new TypeVar\n\n# Internal mypy fallback type for all typed dicts (does not exist at runtime)\n# N.B. Keep this mostly in sync with typing._TypedDict/mypy_extensions._TypedDict\n@type_check_only\nclass _TypedDict(Mapping[str, object], metaclass=abc.ABCMeta):\n    __required_keys__: ClassVar[frozenset[str]]\n    __optional_keys__: ClassVar[frozenset[str]]\n    __total__: ClassVar[bool]\n    __orig_bases__: ClassVar[tuple[Any, ...]]\n    # PEP 705\n    __readonly_keys__: ClassVar[frozenset[str]]\n    __mutable_keys__: ClassVar[frozenset[str]]\n    # PEP 728\n    __closed__: ClassVar[bool | None]\n    __extra_items__: ClassVar[AnnotationForm]\n    def copy(self) -> Self: ...\n    # Using Never so that only calls using mypy plugin hook that specialize the signature\n    # can go through.\n    def setdefault(self, k: Never, default: object) -> object: ...\n    # Mypy plugin hook for 'pop' expects that 'default' has a type variable type.\n    def pop(self, k: Never, default: _T = ...) -> object: ...  # pyright: ignore[reportInvalidTypeVarUse]\n    def update(self, m: Self, /) -> None: ...\n    def items(self) -> dict_items[str, object]: ...\n    def keys(self) -> dict_keys[str, object]: ...\n    def values(self) -> dict_values[str, object]: ...\n    def __delitem__(self, k: Never) -> None: ...\n    @overload\n    def __or__(self, value: Self, /) -> Self: ...\n    @overload\n    def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ...\n    @overload\n    def __ror__(self, value: Self, /) -> Self: ...\n    @overload\n    def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ...\n    # supposedly incompatible definitions of `__ior__` and `__or__`:\n    # Since this module defines \"Self\" it is not recognized by Ruff as typing_extensions.Self\n    def __ior__(self, value: Self, /) -> Self: ...  # type: ignore[misc]\n\nOrderedDict = _Alias()\n\nif sys.version_info >= (3, 13):\n    from typing import get_type_hints as get_type_hints\nelse:\n    def get_type_hints(\n        obj: Any,\n        globalns: dict[str, Any] | None = None,\n        localns: Mapping[str, Any] | None = None,\n        include_extras: bool = False,\n    ) -> dict[str, AnnotationForm]: ...\n\ndef get_args(tp: AnnotationForm) -> tuple[AnnotationForm, ...]: ...\n\nif sys.version_info >= (3, 10):\n    @overload\n    def get_origin(tp: UnionType) -> type[UnionType]: ...\n\n@overload\ndef get_origin(tp: GenericAlias) -> type: ...\n@overload\ndef get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ...\n@overload\ndef get_origin(tp: AnnotationForm) -> AnnotationForm | None: ...\n\nAnnotated: _SpecialForm\n_AnnotatedAlias: Any  # undocumented\n\n# New and changed things in 3.10\nif sys.version_info >= (3, 10):\n    from typing import (\n        Concatenate as Concatenate,\n        ParamSpecArgs as ParamSpecArgs,\n        ParamSpecKwargs as ParamSpecKwargs,\n        TypeAlias as TypeAlias,\n        TypeGuard as TypeGuard,\n        is_typeddict as is_typeddict,\n    )\nelse:\n    @final\n    class ParamSpecArgs:\n        @property\n        def __origin__(self) -> ParamSpec: ...\n        def __init__(self, origin: ParamSpec) -> None: ...\n\n    @final\n    class ParamSpecKwargs:\n        @property\n        def __origin__(self) -> ParamSpec: ...\n        def __init__(self, origin: ParamSpec) -> None: ...\n\n    Concatenate: _SpecialForm\n    TypeAlias: _SpecialForm\n    TypeGuard: _SpecialForm\n    def is_typeddict(tp: object) -> bool: ...\n\n# New and changed things in 3.11\nif sys.version_info >= (3, 11):\n    from typing import (\n        LiteralString as LiteralString,\n        NamedTuple as NamedTuple,\n        Never as Never,\n        NewType as NewType,\n        NotRequired as NotRequired,\n        Required as Required,\n        Self as Self,\n        Unpack as Unpack,\n        assert_never as assert_never,\n        assert_type as assert_type,\n        clear_overloads as clear_overloads,\n        dataclass_transform as dataclass_transform,\n        get_overloads as get_overloads,\n        reveal_type as reveal_type,\n    )\nelse:\n    Self: _SpecialForm\n    Never: _SpecialForm\n    def reveal_type(obj: _T, /) -> _T: ...\n    def assert_never(arg: Never, /) -> Never: ...\n    def assert_type(val: _T, typ: AnnotationForm, /) -> _T: ...\n    def clear_overloads() -> None: ...\n    def get_overloads(func: Callable[..., object]) -> Sequence[Callable[..., object]]: ...\n\n    Required: _SpecialForm\n    NotRequired: _SpecialForm\n    LiteralString: _SpecialForm\n    Unpack: _SpecialForm\n\n    def dataclass_transform(\n        *,\n        eq_default: bool = True,\n        order_default: bool = False,\n        kw_only_default: bool = False,\n        frozen_default: bool = False,\n        field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (),\n        **kwargs: object,\n    ) -> IdentityFunction: ...\n\n    class NamedTuple(tuple[Any, ...]):\n        _field_defaults: ClassVar[dict[str, Any]]\n        _fields: ClassVar[tuple[str, ...]]\n        __orig_bases__: ClassVar[tuple[Any, ...]]\n        @overload\n        def __init__(self, typename: str, fields: Iterable[tuple[str, Any]] = ...) -> None: ...\n        @overload\n        def __init__(self, typename: str, fields: None = None, **kwargs: Any) -> None: ...\n        @classmethod\n        def _make(cls, iterable: Iterable[Any]) -> Self: ...\n        def _asdict(self) -> dict[str, Any]: ...\n        def _replace(self, **kwargs: Any) -> Self: ...\n\n    class NewType:\n        def __init__(self, name: str, tp: AnnotationForm) -> None: ...\n        def __call__(self, obj: _T, /) -> _T: ...\n        __supertype__: type | NewType\n        __name__: str\n        if sys.version_info >= (3, 10):\n            def __or__(self, other: Any) -> _SpecialForm: ...\n            def __ror__(self, other: Any) -> _SpecialForm: ...\n\nif sys.version_info >= (3, 12):\n    from collections.abc import Buffer as Buffer\n    from types import get_original_bases as get_original_bases\n    from typing import (\n        SupportsAbs as SupportsAbs,\n        SupportsBytes as SupportsBytes,\n        SupportsComplex as SupportsComplex,\n        SupportsFloat as SupportsFloat,\n        SupportsIndex as SupportsIndex,\n        SupportsInt as SupportsInt,\n        SupportsRound as SupportsRound,\n        override as override,\n    )\nelse:\n    def override(arg: _F, /) -> _F: ...\n    def get_original_bases(cls: type, /) -> tuple[Any, ...]: ...\n\n    # mypy and pyright object to this being both ABC and Protocol.\n    # At runtime it inherits from ABC and is not a Protocol, but it is on the\n    # allowlist for use as a Protocol.\n    @runtime_checkable\n    class Buffer(Protocol, abc.ABC):  # type: ignore[misc]  # pyright: ignore[reportGeneralTypeIssues]\n        # Not actually a Protocol at runtime; see\n        # https://github.com/python/typeshed/issues/10224 for why we're defining it this way\n        def __buffer__(self, flags: int, /) -> memoryview: ...\n\n    @runtime_checkable\n    class SupportsInt(Protocol, metaclass=abc.ABCMeta):\n        __slots__ = ()\n        @abc.abstractmethod\n        def __int__(self) -> int: ...\n\n    @runtime_checkable\n    class SupportsFloat(Protocol, metaclass=abc.ABCMeta):\n        __slots__ = ()\n        @abc.abstractmethod\n        def __float__(self) -> float: ...\n\n    @runtime_checkable\n    class SupportsComplex(Protocol, metaclass=abc.ABCMeta):\n        __slots__ = ()\n        @abc.abstractmethod\n        def __complex__(self) -> complex: ...\n\n    @runtime_checkable\n    class SupportsBytes(Protocol, metaclass=abc.ABCMeta):\n        __slots__ = ()\n        @abc.abstractmethod\n        def __bytes__(self) -> bytes: ...\n\n    @runtime_checkable\n    class SupportsIndex(Protocol, metaclass=abc.ABCMeta):\n        __slots__ = ()\n        @abc.abstractmethod\n        def __index__(self) -> int: ...\n\n    @runtime_checkable\n    class SupportsAbs(Protocol[_T_co]):\n        __slots__ = ()\n        @abc.abstractmethod\n        def __abs__(self) -> _T_co: ...\n\n    @runtime_checkable\n    class SupportsRound(Protocol[_T_co]):\n        __slots__ = ()\n        @overload\n        @abc.abstractmethod\n        def __round__(self) -> int: ...\n        @overload\n        @abc.abstractmethod\n        def __round__(self, ndigits: int, /) -> _T_co: ...\n\nif sys.version_info >= (3, 14):\n    from io import Reader as Reader, Writer as Writer\nelse:\n    @runtime_checkable\n    class Reader(Protocol[_T_co]):\n        __slots__ = ()\n        @abc.abstractmethod\n        def read(self, size: int = ..., /) -> _T_co: ...\n\n    @runtime_checkable\n    class Writer(Protocol[_T_contra]):\n        __slots__ = ()\n        @abc.abstractmethod\n        def write(self, data: _T_contra, /) -> int: ...\n\nif sys.version_info >= (3, 13):\n    from types import CapsuleType as CapsuleType\n    from typing import (\n        NoDefault as NoDefault,\n        ParamSpec as ParamSpec,\n        ReadOnly as ReadOnly,\n        TypeIs as TypeIs,\n        TypeVar as TypeVar,\n        TypeVarTuple as TypeVarTuple,\n        get_protocol_members as get_protocol_members,\n        is_protocol as is_protocol,\n    )\n    from warnings import deprecated as deprecated\nelse:\n    def is_protocol(tp: type, /) -> bool: ...\n    def get_protocol_members(tp: type, /) -> frozenset[str]: ...\n    @final\n    @type_check_only\n    class _NoDefaultType: ...\n\n    NoDefault: _NoDefaultType\n    @final\n    class CapsuleType: ...\n\n    class deprecated:\n        message: LiteralString\n        category: type[Warning] | None\n        stacklevel: int\n        def __init__(\n            self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1\n        ) -> None: ...\n        def __call__(self, arg: _T, /) -> _T: ...\n\n    @final\n    class TypeVar:\n        @property\n        def __name__(self) -> str: ...\n        @property\n        def __bound__(self) -> AnnotationForm | None: ...\n        @property\n        def __constraints__(self) -> tuple[AnnotationForm, ...]: ...\n        @property\n        def __covariant__(self) -> bool: ...\n        @property\n        def __contravariant__(self) -> bool: ...\n        @property\n        def __infer_variance__(self) -> bool: ...\n        @property\n        def __default__(self) -> AnnotationForm: ...\n        def __init__(\n            self,\n            name: str,\n            *constraints: AnnotationForm,\n            bound: AnnotationForm | None = None,\n            covariant: bool = False,\n            contravariant: bool = False,\n            default: AnnotationForm = ...,\n            infer_variance: bool = False,\n        ) -> None: ...\n        def has_default(self) -> bool: ...\n        def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ...\n        if sys.version_info >= (3, 10):\n            def __or__(self, right: Any) -> _SpecialForm: ...\n            def __ror__(self, left: Any) -> _SpecialForm: ...\n        if sys.version_info >= (3, 11):\n            def __typing_subst__(self, arg: Any) -> Any: ...\n\n    @final\n    class ParamSpec:\n        @property\n        def __name__(self) -> str: ...\n        @property\n        def __bound__(self) -> AnnotationForm | None: ...\n        @property\n        def __covariant__(self) -> bool: ...\n        @property\n        def __contravariant__(self) -> bool: ...\n        @property\n        def __infer_variance__(self) -> bool: ...\n        @property\n        def __default__(self) -> AnnotationForm: ...\n        def __init__(\n            self,\n            name: str,\n            *,\n            bound: None | AnnotationForm | str = None,\n            contravariant: bool = False,\n            covariant: bool = False,\n            default: AnnotationForm = ...,\n        ) -> None: ...\n        @property\n        def args(self) -> ParamSpecArgs: ...\n        @property\n        def kwargs(self) -> ParamSpecKwargs: ...\n        def has_default(self) -> bool: ...\n        def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ...\n        if sys.version_info >= (3, 10):\n            def __or__(self, right: Any) -> _SpecialForm: ...\n            def __ror__(self, left: Any) -> _SpecialForm: ...\n\n    @final\n    class TypeVarTuple:\n        @property\n        def __name__(self) -> str: ...\n        @property\n        def __default__(self) -> AnnotationForm: ...\n        def __init__(self, name: str, *, default: AnnotationForm = ...) -> None: ...\n        def __iter__(self) -> Any: ...  # Unpack[Self]\n        def has_default(self) -> bool: ...\n        def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ...\n\n    ReadOnly: _SpecialForm\n    TypeIs: _SpecialForm\n\n# TypeAliasType was added in Python 3.12, but had significant changes in 3.14.\nif sys.version_info >= (3, 14):\n    from typing import TypeAliasType as TypeAliasType\nelse:\n    @final\n    class TypeAliasType:\n        def __init__(\n            self, name: str, value: AnnotationForm, *, type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = ()\n        ) -> None: ...\n        @property\n        def __value__(self) -> AnnotationForm: ...\n        @property\n        def __type_params__(self) -> tuple[TypeVar | ParamSpec | TypeVarTuple, ...]: ...\n        @property\n        # `__parameters__` can include special forms if a `TypeVarTuple` was\n        # passed as a `type_params` element to the constructor method.\n        def __parameters__(self) -> tuple[TypeVar | ParamSpec | AnnotationForm, ...]: ...\n        @property\n        def __name__(self) -> str: ...\n        # It's writable on types, but not on instances of TypeAliasType.\n        @property\n        def __module__(self) -> str | None: ...  # type: ignore[override]\n        # Returns typing._GenericAlias, which isn't stubbed.\n        def __getitem__(self, parameters: Incomplete | tuple[Incomplete, ...]) -> AnnotationForm: ...\n        def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> NoReturn: ...\n        if sys.version_info >= (3, 10):\n            def __or__(self, right: Any, /) -> _SpecialForm: ...\n            def __ror__(self, left: Any, /) -> _SpecialForm: ...\n\n# PEP 727\nclass Doc:\n    documentation: str\n    def __init__(self, documentation: str, /) -> None: ...\n    def __hash__(self) -> int: ...\n    def __eq__(self, other: object) -> bool: ...\n\n# PEP 728\n@type_check_only\nclass _NoExtraItemsType: ...\n\nNoExtraItems: _NoExtraItemsType\n\n# PEP 747\nTypeForm: _SpecialForm\n\n# PEP 649/749\nif sys.version_info >= (3, 14):\n    from typing import evaluate_forward_ref as evaluate_forward_ref\n\n    from annotationlib import Format as Format, get_annotations as get_annotations, type_repr as type_repr\nelse:\n    class Format(enum.IntEnum):\n        VALUE = 1\n        VALUE_WITH_FAKE_GLOBALS = 2\n        FORWARDREF = 3\n        STRING = 4\n\n    @overload\n    def get_annotations(\n        obj: Any,  # any object with __annotations__ or __annotate__\n        *,\n        globals: Mapping[str, Any] | None = None,  # value types depend on the key\n        locals: Mapping[str, Any] | None = None,  # value types depend on the key\n        eval_str: bool = False,\n        format: Literal[Format.STRING],\n    ) -> dict[str, str]: ...\n    @overload\n    def get_annotations(\n        obj: Any,  # any object with __annotations__ or __annotate__\n        *,\n        globals: Mapping[str, Any] | None = None,  # value types depend on the key\n        locals: Mapping[str, Any] | None = None,  # value types depend on the key\n        eval_str: bool = False,\n        format: Literal[Format.FORWARDREF],\n    ) -> dict[str, AnnotationForm | ForwardRef]: ...\n    @overload\n    def get_annotations(\n        obj: Any,  # any object with __annotations__ or __annotate__\n        *,\n        globals: Mapping[str, Any] | None = None,  # value types depend on the key\n        locals: Mapping[str, Any] | None = None,  # value types depend on the key\n        eval_str: bool = False,\n        format: Format = Format.VALUE,\n    ) -> dict[str, AnnotationForm]: ...\n    @overload\n    def evaluate_forward_ref(\n        forward_ref: ForwardRef,\n        *,\n        owner: Callable[..., object] | type[object] | ModuleType | None = None,  # any callable, class, or module\n        globals: Mapping[str, Any] | None = None,  # value types depend on the key\n        locals: Mapping[str, Any] | None = None,  # value types depend on the key\n        type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None,\n        format: Literal[Format.STRING],\n        _recursive_guard: Container[str] = ...,\n    ) -> str: ...\n    @overload\n    def evaluate_forward_ref(\n        forward_ref: ForwardRef,\n        *,\n        owner: Callable[..., object] | type[object] | ModuleType | None = None,  # any callable, class, or module\n        globals: Mapping[str, Any] | None = None,  # value types depend on the key\n        locals: Mapping[str, Any] | None = None,  # value types depend on the key\n        type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None,\n        format: Literal[Format.FORWARDREF],\n        _recursive_guard: Container[str] = ...,\n    ) -> AnnotationForm | ForwardRef: ...\n    @overload\n    def evaluate_forward_ref(\n        forward_ref: ForwardRef,\n        *,\n        owner: Callable[..., object] | type[object] | ModuleType | None = None,  # any callable, class, or module\n        globals: Mapping[str, Any] | None = None,  # value types depend on the key\n        locals: Mapping[str, Any] | None = None,  # value types depend on the key\n        type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None,\n        format: Format | None = None,\n        _recursive_guard: Container[str] = ...,\n    ) -> AnnotationForm: ...\n    def type_repr(value: object) -> str: ...\n\n# PEP 661\nclass Sentinel:\n    def __init__(self, name: str, repr: str | None = None) -> None: ...\n    if sys.version_info >= (3, 14):\n        def __or__(self, other: Any) -> UnionType: ...  # other can be any type form legal for unions\n        def __ror__(self, other: Any) -> UnionType: ...  # other can be any type form legal for unions\n    elif sys.version_info >= (3, 10):\n        def __or__(self, other: Any) -> _SpecialForm: ...  # other can be any type form legal for unions\n        def __ror__(self, other: Any) -> _SpecialForm: ...  # other can be any type form legal for unions\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Monty Examples\n\nNumerous examples of what monty can do, and how.\n"
  },
  {
    "path": "examples/expense_analysis/README.md",
    "content": "# Team Expense Analysis\n\nFrom [this](https://platform.claude.com/cookbook/tool-use-programmatic-tool-calling-ptc#understanding-the-third-party-api) Anthropic example.\n"
  },
  {
    "path": "examples/expense_analysis/data.py",
    "content": "from typing import Any\n\nteam_members = [\n    {'id': 1, 'name': 'Alice Chen'},\n    {'id': 2, 'name': 'Bob Smith'},\n    {'id': 3, 'name': 'Carol Jones'},\n    {'id': 4, 'name': 'David Kim'},\n    {'id': 5, 'name': 'Eve Wilson'},\n]\n\n# Simulated expense data (multiple line items per person to bloat traditional context)\nexpenses = {\n    1: [  # Alice - under budget\n        {'date': '2024-07-15', 'amount': 450.00, 'description': 'Flight to NYC'},\n        {'date': '2024-07-16', 'amount': 200.00, 'description': 'Hotel NYC'},\n        {'date': '2024-07-17', 'amount': 85.00, 'description': 'Meals NYC'},\n        {'date': '2024-08-20', 'amount': 380.00, 'description': 'Flight to Chicago'},\n        {'date': '2024-08-21', 'amount': 175.00, 'description': 'Hotel Chicago'},\n        {'date': '2024-09-05', 'amount': 520.00, 'description': 'Flight to Seattle'},\n        {'date': '2024-09-06', 'amount': 225.00, 'description': 'Hotel Seattle'},\n        {'date': '2024-09-07', 'amount': 95.00, 'description': 'Meals Seattle'},\n    ],\n    2: [  # Bob - over standard budget but has custom budget\n        {'date': '2024-07-01', 'amount': 850.00, 'description': 'Flight to London'},\n        {'date': '2024-07-02', 'amount': 450.00, 'description': 'Hotel London'},\n        {'date': '2024-07-03', 'amount': 125.00, 'description': 'Meals London'},\n        {'date': '2024-07-04', 'amount': 450.00, 'description': 'Hotel London'},\n        {'date': '2024-07-05', 'amount': 120.00, 'description': 'Meals London'},\n        {'date': '2024-08-10', 'amount': 780.00, 'description': 'Flight to Tokyo'},\n        {'date': '2024-08-11', 'amount': 380.00, 'description': 'Hotel Tokyo'},\n        {'date': '2024-08-12', 'amount': 380.00, 'description': 'Hotel Tokyo'},\n        {'date': '2024-08-13', 'amount': 150.00, 'description': 'Meals Tokyo'},\n        {'date': '2024-09-15', 'amount': 920.00, 'description': 'Flight to Singapore'},\n        {'date': '2024-09-16', 'amount': 320.00, 'description': 'Hotel Singapore'},\n        {'date': '2024-09-17', 'amount': 320.00, 'description': 'Hotel Singapore'},\n        {'date': '2024-09-18', 'amount': 180.00, 'description': 'Meals Singapore'},\n    ],\n    3: [  # Carol - way over budget (no custom budget)\n        {'date': '2024-07-08', 'amount': 1200.00, 'description': 'Flight to Paris'},\n        {'date': '2024-07-09', 'amount': 550.00, 'description': 'Hotel Paris'},\n        {'date': '2024-07-10', 'amount': 550.00, 'description': 'Hotel Paris'},\n        {'date': '2024-07-11', 'amount': 550.00, 'description': 'Hotel Paris'},\n        {'date': '2024-07-12', 'amount': 200.00, 'description': 'Meals Paris'},\n        {'date': '2024-08-25', 'amount': 1100.00, 'description': 'Flight to Sydney'},\n        {'date': '2024-08-26', 'amount': 480.00, 'description': 'Hotel Sydney'},\n        {'date': '2024-08-27', 'amount': 480.00, 'description': 'Hotel Sydney'},\n        {'date': '2024-08-28', 'amount': 480.00, 'description': 'Hotel Sydney'},\n        {'date': '2024-08-29', 'amount': 220.00, 'description': 'Meals Sydney'},\n        {'date': '2024-09-20', 'amount': 650.00, 'description': 'Flight to Denver'},\n        {'date': '2024-09-21', 'amount': 280.00, 'description': 'Hotel Denver'},\n    ],\n    4: [  # David - slightly under budget\n        {'date': '2024-07-22', 'amount': 420.00, 'description': 'Flight to Boston'},\n        {'date': '2024-07-23', 'amount': 190.00, 'description': 'Hotel Boston'},\n        {'date': '2024-07-24', 'amount': 75.00, 'description': 'Meals Boston'},\n        {'date': '2024-08-05', 'amount': 510.00, 'description': 'Flight to Austin'},\n        {'date': '2024-08-06', 'amount': 210.00, 'description': 'Hotel Austin'},\n        {'date': '2024-08-07', 'amount': 90.00, 'description': 'Meals Austin'},\n        {'date': '2024-09-12', 'amount': 480.00, 'description': 'Flight to Portland'},\n        {'date': '2024-09-13', 'amount': 195.00, 'description': 'Hotel Portland'},\n        {'date': '2024-09-14', 'amount': 85.00, 'description': 'Meals Portland'},\n    ],\n    5: [  # Eve - over standard budget (no custom budget)\n        {'date': '2024-07-03', 'amount': 680.00, 'description': 'Flight to Miami'},\n        {'date': '2024-07-04', 'amount': 320.00, 'description': 'Hotel Miami'},\n        {'date': '2024-07-05', 'amount': 320.00, 'description': 'Hotel Miami'},\n        {'date': '2024-07-06', 'amount': 145.00, 'description': 'Meals Miami'},\n        {'date': '2024-08-18', 'amount': 750.00, 'description': 'Flight to San Diego'},\n        {'date': '2024-08-19', 'amount': 290.00, 'description': 'Hotel San Diego'},\n        {'date': '2024-08-20', 'amount': 290.00, 'description': 'Hotel San Diego'},\n        {'date': '2024-08-21', 'amount': 130.00, 'description': 'Meals San Diego'},\n        {'date': '2024-09-08', 'amount': 820.00, 'description': 'Flight to Las Vegas'},\n        {'date': '2024-09-09', 'amount': 380.00, 'description': 'Hotel Las Vegas'},\n        {'date': '2024-09-10', 'amount': 380.00, 'description': 'Hotel Las Vegas'},\n        {'date': '2024-09-11', 'amount': 175.00, 'description': 'Meals Las Vegas'},\n    ],\n}\n\n# Custom budgets (only Bob has one)\ncustom_budgets = {\n    2: {'amount': 7000.00, 'reason': 'International travel required'},\n}\n\n\nasync def get_team_members(department: str) -> dict[str, Any]:\n    \"\"\"Get list of team members for a department.\n\n    Args:\n        department: The department name (e.g., \"Engineering\").\n\n    Returns:\n        Dictionary with list of team members.\n    \"\"\"\n    return {'department': department, 'members': team_members}\n\n\nasync def get_expenses(user_id: int, quarter: str, category: str) -> dict[str, Any]:\n    \"\"\"Get expense line items for a user.\n\n    Args:\n        user_id: The user's ID.\n        quarter: The quarter (e.g., \"Q3\").\n        category: The expense category (e.g., \"travel\").\n\n    Returns:\n        Dictionary with expense items.\n    \"\"\"\n    items = expenses.get(user_id, [])\n    return {'user_id': user_id, 'quarter': quarter, 'category': category, 'expenses': items}\n\n\nasync def get_custom_budget(user_id: int) -> dict[str, Any] | None:\n    \"\"\"Get custom budget for a user if they have one.\n\n    Args:\n        user_id: The user's ID.\n\n    Returns:\n        Custom budget info or None if no custom budget.\n    \"\"\"\n    budget_info = custom_budgets.get(user_id)\n    if budget_info:\n        return {'user_id': user_id, 'budget': budget_info['amount'], 'reason': budget_info['reason']}\n    return None\n"
  },
  {
    "path": "examples/expense_analysis/main.py",
    "content": "import data\n\nimport pydantic_monty\n\ntype_definitions = '''\nfrom typing import Any\n\nasync def get_team_members(department: str) -> dict[str, Any]:\n    \"\"\"Get list of team members for a department.\n    Args:\n        department: The department name (e.g., \"Engineering\").\n    Returns:\n        Dictionary with list of team members.\n    \"\"\"\n    ...\n\nasync def get_expenses(user_id: int, quarter: str, category: str) -> dict[str, Any]:\n    \"\"\"Get expense line items for a user.\n    Args:\n        user_id: The user's ID.\n        quarter: The quarter (e.g., \"Q3\").\n        category: The expense category (e.g., \"travel\").\n    Returns:\n        Dictionary with expense items.\n    \"\"\"\n    ...\n\nasync def get_custom_budget(user_id: int) -> dict[str, Any] | None:\n    \"\"\"Get custom budget for a user if they have one.\n    Args:\n        user_id: The user's ID.\n    Returns:\n        Custom budget info or None if no custom budget.\n    \"\"\"\n    ...\n'''\n\ncode = \"\"\"\n# Get Engineering team members\nteam_data = await get_team_members(department=\"Engineering\")\nteam_members = team_data.get(\"members\", [])\n\n# Standard budget\nSTANDARD_BUDGET = 5000\n\n# Process each team member\ntotal_members = len(team_members)\nover_budget_list = []\n\nfor member in team_members:\n    user_id = member.get(\"id\")\n    name = member.get(\"name\")\n\n    # Get Q3 travel expenses for this user\n    expenses_data = await get_expenses(user_id=user_id, quarter=\"Q3\", category=\"travel\")\n    expense_items = expenses_data.get(\"expenses\", [])\n\n    # Sum up total expenses\n    total_spent = sum(item.get(\"amount\", 0) for item in expense_items)\n\n    # Check if they exceeded standard budget\n    if total_spent > STANDARD_BUDGET:\n        # Check for custom budget\n        custom_budget_data = await get_custom_budget(user_id=user_id)\n\n        if custom_budget_data is not None:\n            budget = custom_budget_data.get(\"budget\", STANDARD_BUDGET)\n        else:\n            budget = STANDARD_BUDGET\n\n        # Check if they exceeded their actual budget (standard or custom)\n        if total_spent > budget:\n            amount_over = total_spent - budget\n            over_budget_list.append({\n                \"name\": name,\n                \"total_spent\": total_spent,\n                \"budget\": budget,\n                \"amount_over\": amount_over\n            })\n\n# Return the analysis\n{\n    \"total_team_members_analyzed\": total_members,\n    \"count_exceeded_budget\": len(over_budget_list),\n    \"over_budget_details\": over_budget_list\n}\n\"\"\"\n\n\nm = pydantic_monty.Monty(\n    code,\n    inputs=['prompt'],\n    script_name='expense.py',\n    type_check=True,\n    type_check_stubs=type_definitions,\n)\n\n\nasync def main():\n    output = await pydantic_monty.run_monty_async(\n        m,\n        inputs={'prompt': 'testing'},\n        external_functions={\n            'get_team_members': data.get_team_members,\n            'get_expenses': data.get_expenses,\n            'get_custom_budget': data.get_custom_budget,\n        },\n    )\n    print(output)\n\n\nif __name__ == '__main__':\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/sql_playground/README.md",
    "content": "# SQL Playground: Customer Sentiment Analysis\n\nThis example demonstrates using Monty for a task that **cannot be solved with a single SQL query**: analyzing customer purchase data (CSV) and correlating it with their social media sentiment (JSON tweets).\n\nData is from <https://github.com/mafudge/datasets>.\n\n## Why This Example is Interesting\n\n1. **Cross-format data joining**: CSV customer data must join with JSON tweets via Twitter handle - requires programmatic data wrangling\n2. **Loop-based external calls**: Sentiment analysis for each tweet happens in a loop - with JSON tool calling this would flood the context window with 50+ results\n3. **In-sandbox computation**: Averages, correlation, and aggregation happen in Python - no need for the LLM to do mental math\n4. **Variable iteration**: Different customers have different numbers of tweets - code handles this naturally\n5. **File sandboxing**: Uses `OSAccess` to mount data files, demonstrating secure file access patterns\n6. **Type checking**: Validates LLM-generated code against type stubs before execution\n\n## To run\n\n```bash\nuv run python examples/sql_playground/main.py\n```\n"
  },
  {
    "path": "examples/sql_playground/external_functions.py",
    "content": "from __future__ import annotations\n\nimport json\nimport tempfile\nfrom dataclasses import dataclass\nfrom pathlib import PurePosixPath\nfrom typing import Any\n\nfrom pydantic_monty import OSAccess\n\ntry:\n    import duckdb\nexcept ImportError as e:\n    raise ImportError('duckdb is required for query_csv. Install with: pip install duckdb') from e\n\n\n@dataclass\nclass ExternalFunctions:\n    fs: OSAccess\n\n    async def query_csv(\n        self, filepath: PurePosixPath, sql: str, parameters: dict[str, Any] | None = None\n    ) -> list[dict[str, Any]]:\n        \"\"\"Execute SQL query on a CSV file using DuckDB.\n\n        Args:\n            filepath: Path to the CSV file in the virtual filesystem.\n            sql: SQL query to execute. The CSV data is available as a table named 'data'.\n            parameters: Optional dictionary of parameters to bind to the SQL query.\n\n        Returns:\n            List of dictionaries, one per row, with column names as keys.\n        \"\"\"\n        # Read CSV content from virtual filesystem\n        content = self.fs.path_read_bytes(filepath)\n\n        # Write to a temporary file for DuckDB to read\n        # (DuckDB's read_csv_auto works best with file paths)\n        with tempfile.NamedTemporaryFile(mode='wb', suffix='.csv') as tmp:\n            tmp.write(content)\n            tmp.flush()\n\n            conn = duckdb.connect(':memory:')\n            # Create table from CSV\n            # NOTE! duckdb (horribly) reads locals as tables, hence `data` here that isn't used\n            data = conn.read_csv(tmp.name)\n            # Execute the user's query\n            result_rel = conn.execute(sql, parameters)\n            del data\n        # Get column names and rows, then convert to list of dicts\n        columns = [desc[0] for desc in result_rel.description]\n        rows = result_rel.fetchall()\n        return [dict(zip(columns, row)) for row in rows]\n\n    async def read_json(self, filepath: PurePosixPath) -> list[Any] | dict[str, Any]:\n        \"\"\"Read and parse a JSON file from the virtual filesystem.\n\n        Args:\n            filepath: Path to the JSON file in the virtual filesystem.\n\n        Returns:\n            Parsed JSON data (list or dict).\n        \"\"\"\n        content = self.fs.path_read_text(filepath)\n        return json.loads(content)\n\n    @staticmethod\n    async def analyze_sentiment(text: str) -> float:\n        \"\"\"Analyze sentiment of text using simple keyword matching.\n\n        This is a basic sentiment analyzer that scores text based on\n        the presence of positive and negative keywords. For production use,\n        you would want to use a proper NLP library or API.\n\n        Args:\n            text: The text to analyze.\n\n        Returns:\n            Sentiment score from -1.0 (very negative) to +1.0 (very positive).\n            A score of 0.0 indicates neutral sentiment.\n\n        Example:\n            >>> await analyze_sentiment('This product is amazing!')\n            0.3\n        \"\"\"\n        positive_words = [\n            'amazing',\n            'great',\n            'love',\n            'thank',\n            'helpful',\n            'a+',\n            'good',\n            'best',\n            'excellent',\n            'awesome',\n            'fantastic',\n            'wonderful',\n            'glad',\n            'enjoy',\n            'better',\n        ]\n        negative_words = [\n            'bad',\n            'angry',\n            'hate',\n            'terrible',\n            'worst',\n            'fraud',\n            'awful',\n            'horrible',\n            'disappointed',\n            'poor',\n            'useless',\n        ]\n\n        score = 0.0\n        text_lower = text.lower()\n\n        for word in positive_words:\n            if word in text_lower:\n                score += 0.3\n\n        for word in negative_words:\n            if word in text_lower:\n                score -= 0.3\n\n        # Clamp score to [-1, 1]\n        return max(-1.0, min(1.0, score))\n"
  },
  {
    "path": "examples/sql_playground/main.py",
    "content": "\"\"\"SQL Playground Example: Customer Sentiment Analysis with SQL and JSON.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom pathlib import Path\n\nfrom external_functions import ExternalFunctions\n\nimport pydantic_monty\nfrom pydantic_monty import MemoryFile, OSAccess\n\n# Path to the mafudge datasets repository (adjust if needed)\nTHIS_DIR = Path(__file__).parent\nREPO_ROOT = THIS_DIR.parent.parent\nMAFUDGE_DATASETS = (REPO_ROOT / '..' / 'mafudge_datasets').resolve()\nassert MAFUDGE_DATASETS.is_dir(), f'mafudge_datasets directory not found at {MAFUDGE_DATASETS}. '\nSANDBOX_CODE_PATH = THIS_DIR / 'sandbox_code.py'\n\nTYPE_STUBS = (THIS_DIR / 'type_stubs.pyi').read_text()\nSANDBOX_CODE = SANDBOX_CODE_PATH.read_text()\n\n# Read file contents\ncustomers_csv = (MAFUDGE_DATASETS / 'customers' / 'customers.csv').read_text()\nsurveys_csv = (MAFUDGE_DATASETS / 'customers' / 'surveys.csv').read_text()\ntweets_json = (MAFUDGE_DATASETS / 'tweets' / 'tweets.json').read_text()\n\n# Create virtual filesystem with mounted files\nfs = OSAccess(\n    [\n        MemoryFile('/data/customers/customers.csv', content=customers_csv),\n        MemoryFile('/data/customers/surveys.csv', content=surveys_csv),\n        MemoryFile('/data/tweets/tweets.json', content=tweets_json),\n    ]\n)\n\n\nasync def main():\n    \"\"\"Run the customer sentiment analysis in the Monty sandbox.\n\n    Returns:\n        List of analysis results for top customers with sentiment scores.\n    \"\"\"\n    # Set up the virtual filesystem with data files\n\n    # Create external functions that can access the filesystem\n    external_funcs = ExternalFunctions(fs)\n\n    # Create the Monty runner with type checking enabled\n    m = pydantic_monty.Monty(\n        SANDBOX_CODE_PATH.read_text(),\n        script_name='sql_playground.py',\n        type_check=True,\n        type_check_stubs=TYPE_STUBS,\n    )\n\n    # Run the analysis with external functions and OS access\n    results = await pydantic_monty.run_monty_async(\n        m,\n        external_functions={\n            'query_csv': external_funcs.query_csv,\n            'read_json': external_funcs.read_json,\n            'analyze_sentiment': external_funcs.analyze_sentiment,\n        },\n        os=fs,\n    )\n\n    if not results:\n        print('No results found. Check if customers have matching Twitter handles and tweets.')\n    for r in results:\n        sentiment_emoji = '😊' if r['avg_sentiment'] > 0 else '😐' if r['avg_sentiment'] == 0 else '😞'\n        print(f'  {r[\"name\"]}')\n        print(f'    Purchases: ${r[\"total_purchases\"]:,}')\n        print(f'    Twitter: @{r[\"twitter\"]}')\n        print(f'    Tweets: {r[\"tweet_count\"]}')\n        print(f'    Sentiment: {r[\"avg_sentiment\"]:+.2f} {sentiment_emoji}')\n        print()\n\n\nif __name__ == '__main__':\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/sql_playground/sandbox_code.py",
    "content": "\"\"\"Sandboxed analysis code that runs inside Monty.\n\nThis code is executed in the Monty sandbox with access to external functions\nfor SQL queries, JSON parsing, and sentiment analysis.\n\"\"\"\n\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from type_stubs import analyze_sentiment, query_csv, read_json\n\n\nasync def main():\n    # Step 1: Query top 10 customers by total purchases\n    print('getting top customers...')\n    top_customers = await query_csv(\n        filepath=Path('/data/customers/customers.csv'),\n        sql=\"\"\"\n        SELECT \"First\", \"Last\", \"Email\", \"Total Purchased\" as TotalPurchased\n        FROM data\n        ORDER BY \"Total Purchased\"\n        DESC LIMIT 10\n        \"\"\",\n    )\n\n    # Step 2: Get their Twitter handles from the survey data\n    emails: list[str] = [c['Email'] for c in top_customers]\n    print('getting twitter handles...')\n    twitter_handles = await query_csv(\n        Path('/data/customers/surveys.csv'),\n        f\"\"\"\n        SELECT \"Email\", \"Twitter Username\" as Twitter\n        FROM data\n        WHERE \"Email\" IN $emails\n        \"\"\",\n        parameters={'emails': emails},\n    )\n    email_to_twitter = {row['Email']: row['Twitter'] for row in twitter_handles}\n\n    # Step 3: Load all tweets\n    tweets = await read_json(filepath=Path('/data/tweets/tweets.json'))\n    assert isinstance(tweets, list)\n\n    print(f'processing {len(top_customers)} customers...')\n\n    # Step 4: For each customer, find their tweets and analyze sentiment\n    results: list[dict[str, object]] = []\n    for customer in top_customers:\n        twitter = email_to_twitter.get(customer['Email'])\n        if not twitter:\n            continue\n\n        # Find tweets by this user\n        user_tweets = [t for t in tweets if t['user'] == twitter]\n        if not user_tweets:\n            continue\n\n        # Analyze sentiment of each tweet\n        sentiments: list[float] = []\n        for tweet in user_tweets:\n            score = await analyze_sentiment(text=tweet['text'])\n            sentiments.append(score)\n\n        # Calculate average sentiment\n        avg_sentiment = sum(sentiments) / len(sentiments)\n        print(f'{customer[\"First\"]} {customer[\"Last\"]} - {avg_sentiment=}')\n\n        results.append(\n            {\n                'name': f'{customer[\"First\"]} {customer[\"Last\"]}',\n                'total_purchases': customer['TotalPurchased'],\n                'twitter': twitter,\n                'tweet_count': len(user_tweets),\n                'avg_sentiment': round(avg_sentiment, 2),\n            }\n        )\n        return results\n\n\n# Return the analysis results\nawait main()  # pyright: ignore\n"
  },
  {
    "path": "examples/sql_playground/type_stubs.pyi",
    "content": "from pathlib import Path\nfrom typing import Any\n\nasync def query_csv(filepath: Path, sql: str, parameters: dict[str, Any] | None = None) -> list[dict[str, Any]]:\n    \"\"\"Execute SQL query on a CSV file using DuckDB.\"\"\"\n    ...\n\nasync def read_json(filepath: Path) -> list[Any] | dict[str, Any]:\n    \"\"\"Read and parse a JSON file.\"\"\"\n    ...\n\nasync def analyze_sentiment(text: str) -> float:\n    \"\"\"Analyze sentiment of text. Returns score from -1.0 to +1.0.\"\"\"\n    ...\n"
  },
  {
    "path": "examples/web_scraper/README.md",
    "content": "# Web Scraper Example\n\nThis example uses Python dataclass APIs for playwright and beautifulsoup to allow the LLM\nto extract price data from the websites of model labs.\n\nWe use Pydantic AI to generate code, but instead of using the `CodeExecutionToolset` type from Pydantic AI,\nwe get the LLM to generate code directly allowing us to use new features of Monty not yet available in Pydantic AI.\n\nLook at `example_code.py` for an example of the kind of code sonnet 4.5 will generate in this case.\n\nRun the example with\n\n```bash\nuv run python -m examples.web_scraper.main\n```\n"
  },
  {
    "path": "examples/web_scraper/browser.py",
    "content": "from __future__ import annotations\n\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, AsyncIterator, Literal\n\nfrom playwright.async_api import Browser as PwBrowser, Page as PwPage, async_playwright\n\nif TYPE_CHECKING:\n    from .external_functions import Page\n\npw_pages: dict[int, PwPage] = {}\n\n\n@asynccontextmanager\nasync def start_browser() -> AsyncIterator[Browser]:\n    async with async_playwright() as p:\n        b = await p.chromium.launch()\n        yield Browser(b)\n        pw_pages.clear()\n        await b.close()\n\n\n@dataclass\nclass Browser:\n    _pw_browser: PwBrowser\n\n    async def open_page(\n        self,\n        url: str,\n        wait_until: Literal['commit', 'domcontentloaded', 'load', 'networkidle'] = 'networkidle',\n    ) -> Page:\n        \"\"\"Open a URL in a headless browser and return a `Page`.\n\n        Use this to load a web page so you can inspect its HTML content.\n\n        Args:\n            url: The URL to navigate to.\n            wait_until: When to consider navigation complete:\n                `'commit'` — after the response is received,\n                `'domcontentloaded'` — after the `DOMContentLoaded` event,\n                `'load'` — after the `load` event,\n                `'networkidle'` — after there are no network connections for 500ms.\n        \"\"\"\n        from .external_functions import Page\n\n        page = await self._pw_browser.new_page()\n        await page.goto(url, wait_until=wait_until)\n        page_id = id(page)\n        pw_pages[page_id] = page\n        return Page(\n            url=page.url,\n            title=await page.title(),\n            html=await page.content(),\n            id=page_id,\n        )\n"
  },
  {
    "path": "examples/web_scraper/example_code.py",
    "content": "import asyncio\n\n# Open the pricing page\npage = await open_page(url)\n\n# Parse the HTML with BeautifulSoup\nsoup = beautiful_soup(page.html)\n\n# Find the main content area that contains pricing information\n# Let's look for tables or structured pricing data\npricing_tables = soup.find_all('table')\n\n# Initialize a list to store all model pricing data\nall_models = []\n\n# Process each table found\nfor table in pricing_tables:\n    # Get all rows in the table\n    rows = table.find_all('tr')\n\n    if len(rows) < 2:  # Skip tables without data rows\n        continue\n\n    # Get headers from the first row\n    header_row = rows[0]\n    headers = [th.get_text(strip=True) for th in header_row.find_all(['th', 'td'])]\n\n    # Process data rows\n    for row in rows[1:]:\n        cells = row.find_all(['td', 'th'])\n        if len(cells) < 2:\n            continue\n\n        # Extract cell values\n        row_data = [cell.get_text(strip=True) for cell in cells]\n\n        # Skip rows that might indicate deprecated models\n        row_text = ' '.join(row_data).lower()\n        if 'deprecated' in row_text or 'legacy' in row_text:\n            continue\n\n        # Create a dictionary for this model\n        model_info = {}\n        for i, value in enumerate(row_data):\n            if i < len(headers):\n                model_info[headers[i]] = value\n            else:\n                model_info[f'column_{i}'] = value\n\n        if model_info:  # Only add if we have data\n            all_models.append(model_info)\n\n# Print the results\nprint(f'Found {len(all_models)} models with pricing data')\nprint('\\nModel pricing information:')\nfor i, model in enumerate(all_models, 1):\n    print(f'\\n{i}. {model}')\n\nall_models\n"
  },
  {
    "path": "examples/web_scraper/external_functions.py",
    "content": "import re\nfrom dataclasses import dataclass, field\nfrom typing import Any, Literal, cast\n\nfrom bs4 import BeautifulSoup, Tag as BsTag\n\nfrom .browser import PwPage, pw_pages\n\n\nasync def open_page(\n    url: str,\n    wait_until: Literal['commit', 'domcontentloaded', 'load', 'networkidle'] = 'networkidle',\n) -> Page:\n    \"\"\"Open a URL in a headless browser and return a `Page`.\n\n    Use this to load a web page so you can inspect its HTML content.\n\n    Args:\n        url: The URL to navigate to.\n        wait_until: When to consider navigation complete:\n            `'commit'` — after the response is received,\n            `'domcontentloaded'` — after the `DOMContentLoaded` event,\n            `'load'` — after the `load` event,\n            `'networkidle'` — after there are no network connections for 500ms.\n    \"\"\"\n    raise NotImplementedError('this is here just to generate stubs, see _generate_stubs in main.py')\n\n\n@dataclass\nclass Page:\n    \"\"\"A snapshot of a Playwright page.\"\"\"\n\n    url: str\n    title: str\n    html: str\n    id: int\n    _pw_page: PwPage = field(init=False)\n\n    def __post_init__(self):\n        self._pw_page = pw_pages[self.id]\n\n    async def go_to(\n        self,\n        url: str,\n        wait_until: Literal['commit', 'domcontentloaded', 'load', 'networkidle'] = 'networkidle',\n    ) -> None:\n        \"\"\"Navigate the page to a new URL.\n\n        Args:\n            url: The URL to navigate to.\n            wait_until: When to consider navigation complete:\n                `'commit'` — after the response is received,\n                `'domcontentloaded'` — after the `DOMContentLoaded` event,\n                `'load'` — after the `load` event,\n                `'networkidle'` — after there are no network connections for 500ms.\n        \"\"\"\n        await self._pw_page.goto(url, wait_until=wait_until)\n\n    async def click(self, selector: str, force: bool = False) -> None:\n        \"\"\"Click an element matching the CSS selector and return the updated page.\n\n        Args:\n            selector: A CSS selector, e.g. `'button.submit'`, `'a[href=\"/next\"]'`.\n            force: If `True`, bypass actionability checks (visibility, pointer-events interception).\n                Useful when an overlay or sticky nav covers the target element.\n        \"\"\"\n        await self._pw_page.click(selector, force=force)\n        await self._pw_page.wait_for_load_state('networkidle')\n\n    async def fill(self, selector: str, value: str) -> None:\n        \"\"\"Fill a form field matching the CSS selector with the given value.\n\n        Args:\n            selector: A CSS selector for an input/textarea, e.g. `'input[name=\"email\"]'`.\n            value: The text to type into the field.\n        \"\"\"\n        await self._pw_page.fill(selector, value)\n        await self._pw_page.wait_for_load_state('networkidle')\n\n    async def select_option(self, selector: str, value: str) -> None:\n        \"\"\"Select an option in a `<select>` element.\n\n        Args:\n            selector: A CSS selector for the select element.\n            value: The value attribute of the option to select.\n        \"\"\"\n        await self._pw_page.select_option(selector, value)\n        await self._pw_page.wait_for_load_state('networkidle')\n\n    async def check(self, selector: str) -> None:\n        \"\"\"Check a checkbox or radio button.\n\n        Args:\n            selector: A CSS selector for the checkbox/radio input.\n        \"\"\"\n        await self._pw_page.check(selector)\n        await self._pw_page.wait_for_load_state('networkidle')\n\n    async def press(self, selector: str, key: str) -> None:\n        \"\"\"Press a keyboard key on a focused element.\n\n        Args:\n            selector: A CSS selector for the element to focus.\n            key: Key to press, e.g. `'Enter'`, `'Tab'`, `'ArrowDown'`.\n        \"\"\"\n        await self._pw_page.press(selector, key)\n        await self._pw_page.wait_for_load_state('networkidle')\n\n    async def wait_for_selector(self, selector: str, timeout: float = 30_000) -> None:\n        \"\"\"Wait for an element matching the CSS selector to appear, then return the updated page.\n\n        Args:\n            selector: A CSS selector to wait for.\n            timeout: Maximum time to wait in milliseconds (default 30 000).\n        \"\"\"\n        await self._pw_page.wait_for_selector(selector, timeout=timeout)\n        await self._pw_page.wait_for_load_state('networkidle')\n\n    async def screenshot(self, full_page: bool = False) -> bytes:\n        \"\"\"Take a screenshot of the current page.\n\n        Args:\n            full_page: If `True`, capture the full scrollable page rather than just the viewport.\n\n        Returns:\n            PNG image bytes.\n        \"\"\"\n        return await self._pw_page.screenshot(full_page=full_page, type='png')\n\n    async def evaluate(self, expression: str) -> str:\n        \"\"\"Evaluate a JavaScript expression on the page and return the result as a string.\n\n        Args:\n            expression: JavaScript to evaluate, e.g. `'document.title'`.\n        \"\"\"\n        result = await self._pw_page.evaluate(expression)\n        return str(result)\n\n    async def get_text(self, selector: str) -> str:\n        \"\"\"Get the text content of the first element matching the CSS selector.\n\n        Args:\n            selector: A CSS selector, e.g. `'h1'`, `'.price'`.\n\n        Returns:\n            The text content of the matched element, or an empty string if not found.\n        \"\"\"\n        text = await self._pw_page.text_content(selector)\n        return text or ''\n\n    async def get_attribute(self, selector: str, name: str) -> str | None:\n        \"\"\"Get an attribute value from the first element matching the CSS selector.\n\n        Args:\n            selector: A CSS selector.\n            name: Attribute name, e.g. `'href'`, `'src'`.\n\n        Returns:\n            The attribute value, or `None` if the element or attribute is not found.\n        \"\"\"\n        return await self._pw_page.get_attribute(selector, name)\n\n\ndef beautiful_soup(html: str) -> Tag:\n    \"\"\"Parse html with BeautifulSoup and return a `Tag`.\n\n    Use this tool to get back a `Tag` object that can be used to extract information from HTML.\n    \"\"\"\n    soup = BeautifulSoup(html, 'html.parser')\n    return _from_beautifulsoup(soup)\n\n\n@dataclass\nclass Tag:\n    \"\"\"A mirror of a BeautifulSoup `Tag`.\"\"\"\n\n    name: str\n    attrs: dict[str, str | list[str]] = field(default_factory=dict)\n    string: str | None = None\n    text: str = ''\n    html: str = ''\n\n    def find(\n        self, name: str | None = None, attrs: dict[str, str] | None = None, string: str | None = None\n    ) -> 'Tag | None':\n        \"\"\"Find the first descendant tag matching the criteria.\n\n        Args:\n            name: Tag name to match, e.g. `'a'`, `'div'`.\n            attrs: Attribute key-value pairs to filter on.\n            string: Match tags whose `.string` equals this value.\n\n        Returns:\n            The first matching `Tag`, or `None` if no match is found.\n        \"\"\"\n        # bs4's types are horrible, this is the easiest work around\n        result = _parse(self.html).find(name, cast(Any, attrs), string=cast(Any, string))\n        if result is None:\n            return None\n        else:\n            return _from_beautifulsoup(result)\n\n    def find_all(\n        self,\n        name: str | re.Pattern[str] | None = None,\n        attrs: dict[str, str] | None = None,\n        string: str | None = None,\n        limit: int | None = None,\n    ) -> 'list[Tag]':\n        \"\"\"Find all descendant tags matching the criteria.\n\n        Args:\n            name: Tag name or compiled regex to match.\n            attrs: Attribute key-value pairs to filter on.\n            string: Match tags whose `.string` equals this value.\n            limit: Stop after finding this many results.\n\n        Returns:\n            A list of matching `Tag` objects.\n        \"\"\"\n        # bs4's types are horrible, this is the easiest work around\n        results = _parse(self.html).find_all(name, cast(Any, attrs), string=cast(Any, string), limit=limit)\n        return [_from_beautifulsoup(r) for r in results]\n\n    def select(self, selector: str) -> 'list[Tag]':\n        \"\"\"Find all descendants matching a CSS selector.\n\n        Args:\n            selector: A CSS selector string, e.g. `'div.class > a'`.\n\n        Returns:\n            A list of matching `Tag` objects.\n        \"\"\"\n        return [_from_beautifulsoup(r) for r in _parse(self.html).select(selector)]\n\n    def select_one(self, selector: str) -> 'Tag | None':\n        \"\"\"Find the first descendant matching a CSS selector.\n\n        Args:\n            selector: A CSS selector string, e.g. `'div.class > a'`.\n\n        Returns:\n            The first matching `Tag`, or `None` if no match is found.\n        \"\"\"\n        result = _parse(self.html).select_one(selector)\n        if result is None:\n            return None\n        return _from_beautifulsoup(result)\n\n    def get(self, key: str, default: str | None = None) -> str | list[str] | None:\n        \"\"\"Get an attribute value by key.\n\n        Args:\n            key: The attribute name, e.g. `'href'`, `'class'`.\n            default: Value to return if the attribute is missing.\n\n        Returns:\n            The attribute value (a `str`, or `list[str]` for multi-valued\n            attributes like `class`), or `default` if not found.\n        \"\"\"\n        return self.attrs.get(key, default)\n\n    def get_text(self, separator: str = '', strip: bool = False) -> str:\n        \"\"\"Extract all text content within this tag.\n\n        Args:\n            separator: String inserted between text fragments.\n            strip: Whether to strip whitespace from each fragment.\n\n        Returns:\n            The concatenated text content.\n        \"\"\"\n        return _parse(self.html).get_text(separator=separator, strip=strip)\n\n    def children(self) -> 'list[Tag | str]':\n        \"\"\"Get the direct children of this tag.\n\n        Returns:\n            A list where each element is either a `Tag` or a `str`\n            (for navigable text nodes).\n        \"\"\"\n        out: list[Tag | str] = []\n        for child in _parse(self.html).children:\n            if isinstance(child, BsTag):\n                out.append(_from_beautifulsoup(child))\n            else:\n                text = str(child)\n                if text:\n                    out.append(text)\n        return out\n\n\n# ------------------------------------------------------------------\n# Public helpers\n# ------------------------------------------------------------------\n\n\ndef _from_beautifulsoup(element: BsTag) -> Tag:\n    \"\"\"Convert a BeautifulSoup `Tag` into a Monty-compatible `Tag` dataclass.\"\"\"\n    assert isinstance(element, BsTag), f'Expected a BeautifulSoup Tag, got {type(element)}'\n    string_val = element.string\n    return Tag(\n        name=element.name,\n        attrs=dict(element.attrs),\n        string=str(string_val) if string_val is not None else None,\n        text=element.get_text(),\n        html=str(element),\n    )\n\n\ndef _parse(html: str) -> BsTag:\n    \"\"\"Re-parse stored HTML into a BeautifulSoup tag.\n\n    If the HTML represents a full document the `BeautifulSoup` object\n    itself is returned (it behaves like a Tag).  Otherwise the first\n    child tag is returned so that `find`/`select` operate on the\n    correct element.\n    \"\"\"\n    soup = BeautifulSoup(html, 'html.parser')\n    # If the html was a single tag, unwrap so searches are scoped correctly.\n    children = list(soup.children)\n    if len(children) == 1 and isinstance(children[0], BsTag):\n        return children[0]\n    return soup\n"
  },
  {
    "path": "examples/web_scraper/main.py",
    "content": "import asyncio\nimport re\nimport subprocess\nimport tempfile\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Literal\n\nimport logfire\nimport pydantic_core\nfrom pydantic_ai import Agent, ModelRequest, ModelRequestNode, UserPromptPart\nfrom pydantic_graph import End\n\nfrom pydantic_monty import Monty, MontyError, MontyRuntimeError, run_monty_async\n\nfrom .browser import start_browser\nfrom .external_functions import beautiful_soup\nfrom .sub_agent import RecordModels\n\nlogfire.configure()\nlogfire.instrument_pydantic_ai()\n\nTHIS_DIR = Path(__file__).parent\n\n\ndef _generate_stubs() -> str:\n    \"\"\"Generate type stubs for external_functions.py using stubgen.\"\"\"\n    with tempfile.TemporaryDirectory() as tmpdir:\n        subprocess.run(\n            ['uv', 'run', 'stubgen', 'external_functions.py', '--include-docstrings', '-o', tmpdir],\n            capture_output=True,\n            text=True,\n            cwd=THIS_DIR,\n            check=True,\n        )\n        return (Path(tmpdir) / 'external_functions.pyi').read_text()\n\n\nstubs = f\"\"\"\n{RecordModels.record_model_info_stub()}\n\n{_generate_stubs()}\n\"\"\"\ninstrunctions = f\"\"\"\nYou MUST return markdown with either a comment and python code to execute\nin a \"```python\" code block, or an explanation of your process to end.\n\nYou MUST return only one code block to execute. DO NOT return multiple code blocks.\n\nYou MUST use the `record_model_info` function to record information about every model you find.\n\nThe runtime uses a restricted Python subset:\n- you cannot use the standard library except builtin functions and the following modules: `sys`, `typing`, `asyncio`\n- this means `json`, `collections`, `json`, `re`, `math`, `datetime`, `itertools`, `functools`, etc. are NOT available  use plain dicts, lists, and builtins instead\n- you cannot use third party libraries\n- you cannot define classes\n- the python executor is NOT a REPL, you must define all values each time you call python\n\nThe last expression evaluated is the return value.\n\nYou can use `print()` to get debug information while developing the code.\n\nParallelism: use `asyncio.gather` to fire multiple calls at the same time instead of awaiting each one sequentially:\n\nYou can use the following types functions and types:\n\n```python\n{stubs}\n```\n\"\"\"\n\nscrape_agent = Agent('gateway/anthropic:claude-sonnet-4-5', instructions=instrunctions)\n\nurls = {\n    'openai': 'https://developers.openai.com/api/docs/pricing',\n    'anthropic': 'https://platform.claude.com/docs/en/about-claude/pricing',\n    'groq': 'https://groq.com/pricing',\n}\n\n\nasync def main(model: str):\n    url = urls[model]\n    prompt = f\"\"\"\nGet structured information including pricing data for all models from the following URL:\n\n{url}\n\nThe HTML returned from this URL is too big for context, so make sure to process it with\nthe functions provided or return a small snippet of the HTML to process.\n\nIgnore any deprecated models.\n\"\"\"\n\n    print_output: list[str] = []\n\n    def monty_print(_: Literal['stdout'], content: str):\n        print_output.append(content)\n\n    record_models = RecordModels()\n\n    async with start_browser() as browser:\n        async with scrape_agent.iter(prompt) as agent_run:\n            node = agent_run.next_node\n            while True:\n                while not isinstance(node, End):\n                    node = await agent_run.next(node)\n\n                extracted = ExtractCode.extract(node.data.output)\n                logfire.info(f'{extracted}')\n                if extracted.comment:\n                    print(f'LLM: {extracted.comment}')\n\n                if not extracted.code:\n                    print('done')\n                    break\n\n                try:\n                    with logfire.span('prepare monty', code=extracted.code):\n                        m = Monty(\n                            extracted.code,\n                            type_check=True,\n                            type_check_stubs=stubs,\n                        )\n                except MontyError as e:\n                    msg = f'Error Preparing Code: {e}'\n                    node = await agent_run.next(new_node(msg))\n                    continue\n\n                try:\n                    with logfire.span('running monty'):\n                        output = await run_monty_async(\n                            m,\n                            external_functions={\n                                'open_page': browser.open_page,\n                                'beautiful_soup': beautiful_soup,\n                                'record_model_info': record_models.record_model_info,\n                            },\n                            print_callback=monty_print,\n                        )\n                except MontyRuntimeError as e:\n                    msg = f'Error running code: {e.display()}'\n                else:\n                    msg = pydantic_core.to_json(output).decode()\n\n                if print_output:\n                    msg += f'\\n\\nPrint Output:\\n---\\n{\"\".join(print_output)}\\n---'\n                    print_output.clear()\n                node = await agent_run.next(new_node(msg))\n\n        logfire.info('{models=}', models=record_models.models)\n\n\ndef new_node(msg: str) -> ModelRequestNode[None, str]:\n    return ModelRequestNode(request=ModelRequest(instructions=instrunctions, parts=[UserPromptPart(content=msg)]))\n\n\n@dataclass\nclass ExtractCode:\n    \"\"\"Extract Python code from an LLM response.\n\n    Priority:\n    1. First ```python code fence\n    2. First code fence of any language\n    3. Entire response as-is\n    \"\"\"\n\n    code: str | None\n    \"\"\"Code extract from response.\n\n    First ```(python|py) code fence, or first unexplained\n\n    \"\"\"\n    comment: str | None\n\n    @classmethod\n    def extract(cls, response: str) -> ExtractCode:\n        # Try ```python or ```py fences first\n        m = re.search(r'```(?:python|py)\\s*\\n(.*?)```', response, re.DOTALL)\n        if not m:\n            # Try any code fence\n            m = re.search(r'```\\w*\\s*\\n(.*?)```', response, re.DOTALL)\n\n        if m:\n            code = m.group(1).strip()\n            # Extract comment as text before the code fence\n            comment = response[: m.start()].strip() or None\n            return cls(code=code, comment=comment)\n\n        return cls(code=None, comment=response.strip())\n\n\nif __name__ == '__main__':\n    asyncio.run(main('anthropic'))\n"
  },
  {
    "path": "examples/web_scraper/sub_agent.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass, field\nfrom textwrap import indent\nfrom typing import Any\n\nfrom pydantic import BaseModel, ValidationError\nfrom pydantic_ai import Agent, ModelRetry, format_as_xml\n\n\nclass ModelInfo(BaseModel, use_attribute_docstrings=True):\n    unique_id: str\n    \"\"\"Unique identifier for the model.\"\"\"\n    name: str\n    \"\"\"Name of the model.\"\"\"\n    description: str | None = None\n    \"\"\"Description of the model.\"\"\"\n    input_mtok: float\n    \"\"\"Input tokens per million tokens.\"\"\"\n    output_mtok: float\n    \"\"\"Output tokens per million tokens.\"\"\"\n    attributes: dict[str, float | str] | None = None\n    \"\"\"Any other attributes of the model.\"\"\"\n\n\nagent = Agent(\n    'gateway/anthropic:claude-sonnet-4-5',\n    output_type=ModelInfo | str,\n    instructions=\"Try to coerce the input into a ModelInfo object, if you're unable to do so, return a string describing the error.\",\n    name='extraction-sub-agent',\n)\n\n\n@dataclass\nclass RecordModels:\n    models: dict[str, ModelInfo] = field(default_factory=dict)\n\n    async def record_model_info(self, model_information: dict[str, Any]) -> str:\n        \"\"\"Record information about a model.\"\"\"\n\n        try:\n            output = ModelInfo.model_validate(model_information)\n        except ValidationError:\n            pass\n        else:\n            self.models[output.unique_id] = output\n            return f'Model information recorded successfully for {output.unique_id}'\n\n        try:\n            result = await agent.run(format_as_xml(model_information))\n        except ModelRetry:\n            return 'Error, unable to validate model information'\n        else:\n            output = result.output\n            if isinstance(output, str):\n                return output\n            else:\n                self.models[output.unique_id] = output\n                return f'Model information recorded successfully for {output.unique_id}'\n\n    @classmethod\n    def record_model_info_stub(cls) -> str:\n        return f'''\\\nfrom typing import Any\n\ndef record_model_info(model_information: dict[str, Any]) -> str:\n    \"\"\"Record information about a model.\n\n    NOTE: this method takes a python dict argument, not JSON.\n\n    The dict should have this schema:\n\n    ```json\n    {indent(json.dumps(ModelInfo.model_json_schema(), indent=2), prefix='    ')}\n    ```\n    \"\"\"\n    ...\n'''\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"monty-workspace\"\nversion = \"0\"\nrequires-python = \">=3.10\"\n\n[tool.uv.workspace]\nmembers = [\"crates/monty-python\"]\n\n[dependency-groups]\ndev = [\n    \"basedpyright>=1.34.0\",\n    \"maturin>=1.9.4,<2.0\",\n    \"mypy>=1.19.1\",\n    \"ruff>=0.14.7\",\n    \"duckdb>=1.0.0\",\n    \"httpx>=0.28.1\",\n    \"playwright>=1.58.0\",\n    \"beautifulsoup4>=4.14.3\",\n    \"logfire>=4.25.0\",\n    \"devtools>=0.12.2\",\n    \"pydantic-ai-slim[anthropic,openai]>=1.63.0\",\n]\n\n# https://beta.ruff.rs/docs/configuration/\n[tool.ruff]\nline-length = 120\ninclude = [\n    \"scripts/**/*.py\",\n    \"crates/**/*.py\",\n    \"crates/**/*.pyi\",\n]\n# Exclude files with intentional syntax errors or special formatting requirements\nextend-exclude = [\n    \"crates/monty/test_cases/nonlocal__error_module_level.py\",\n    \"crates/monty/test_cases/traceback__nonlocal_module_scope.py\",\n    # These files test specific quote styles in error messages and must not be reformatted\n    \"crates/monty/test_cases/type__float_repr_newline.py\",\n    \"crates/monty/test_cases/type__float_repr_both_quotes.py\",\n]\n\n[tool.ruff.lint]\nextend-select = [\n    \"Q\",      # https://docs.astral.sh/ruff/rules/#flake8-quotes-q\n    \"RUF100\", # https://docs.astral.sh/ruff/rules/unused-noqa/\n    \"I\",      # https://docs.astral.sh/ruff/rules/#isort-i\n    \"FA\",     # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa\n]\nflake8-quotes = { inline-quotes = \"single\", multiline-quotes = \"double\" }\nmccabe = { max-complexity = 14 }\n\n[tool.ruff.lint.isort]\ncombine-as-imports = true\nknown-first-party = [\"pydantic_monty\"]\n\n[tool.ruff.lint.per-file-ignores]\n\"crates/monty/test_cases/*.py\" = [\n    \"E722\", # bare except - needed for testing try/except with bare except clauses\n    \"E711\", # Comparison to `None` should be `cond is None`\n    \"E712\", # Comparison to True/False - needed for testing boolean behavior\n    \"E714\", # Test for object identity should be `is not`\n    \"E721\", # Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks\n    \"E731\", # Do not assign a lambda expression, use a def - needed for testing lambda expressions\n    \"F541\", # f-string without any placeholders\n    \"F632\", # ignore replace `is` with `==` - we have lots of these tests\n    \"F701\", # break outside loop - needed for testing break error handling\n    \"F702\", # continue not properly in loop - needed for testing continue error handling\n    \"F704\", # await outside function - needed for testing top-level async/await\n    \"F811\", # Redefinition of unused variable\n    \"F821\", # traceback undefined call test intentionally uses undefined name\n    \"F841\", # unused variable - needed for testing exception variable cleanup\n    \"F401\", # Intentionally imports unknown typing constructs to test silent ignore behavior\n    \"F403\", # star import test intentionally uses `from sys import *`\n    \"E402\", # Typing test file has multiple import sections to test different import patterns\n]\n\n\"crates/monty-typeshed/vendor/typeshed/stdlib/**/*.pyi\" = [\n    \"F403\", # unable to detect undefined names\n    \"F821\", # Undefined name\n]\n\"crates/monty-type-checking/tests/good_types.py\" = [\n    \"F704\", # await outside function - needed for testing top-level async/await\n]\n\n[tool.ruff.format]\ndocstring-code-format = true\nquote-style = \"single\"\n\n[tool.pyright]\npythonVersion = \"3.14\"\ntypeCheckingMode = \"strict\"\nreportUnnecessaryTypeIgnoreComment = true\ninclude = [\"scripts\", \"crates/monty/test_cases\", \"crates/monty-python\", \"crates/monty-type-checking\", \"examples\"]\nexclude = [\n    \"crates/monty-type-checking/tests/bad_types.py\",\n    \"crates/monty-type-checking/tests/reveal_types.py\",\n    # break/continue outside loop tests intentionally test error handling\n    \"crates/monty/test_cases/loop__break_outside_error.py\",\n    \"crates/monty/test_cases/loop__continue_outside_error.py\",\n    \"crates/monty/test_cases/loop__break_in_function_error.py\",\n    \"crates/monty/test_cases/loop__continue_in_function_error.py\",\n    \"crates/monty/test_cases/loop__break_in_if_error.py\",\n    \"crates/monty/test_cases/loop__continue_in_if_error.py\",\n    \"scripts/startup_performance.py\",\n    \"examples/web_scraper/example_code.py\",\n]\nvenv = \".venv\"\n\n# we include tests cases in type checking then switch off all rules that break\n# so the files don't show as errors in the IDE\n[[tool.pyright.executionEnvironments]]\nroot = \"crates/monty/test_cases\"\nreportUnusedExpression = false\nreportOperatorIssue = false\nreportGeneralTypeIssues = false\nreportMissingImports = false\nreportUnknownMemberType = false\nreportUnnecessaryComparison = false\nreportUnnecessaryIsInstance = false\nreportUnknownArgumentType = false\nreportUnknownVariableType = false\nreportUnhashable = false\nreportCallIssue = false\nreportUnknownParameterType = false\nreportMissingParameterType = false\nreportUnknownLambdaType = false\nreportArgumentType = false\n# iter mode tests use external functions defined in the test runner\nreportUndefinedVariable = false\n# loop variable persistence tests have \"possibly unbound\" variables\nreportPossiblyUnboundVariable = false\n# exception variable cleanup tests intentionally access unbound variables\nreportUnboundVariable = false\n# attribute error tests intentionally access/assign non-existent attributes\nreportAttributeAccessIssue = false\n# unused variables and imports are fine in test cases\nreportUnusedVariable = false\nreportUnusedImport = false\nreportAssignmentType = false\nreportFunctionMemberAccess = false\n# ~bool is deprecated in Python 3.16 but valid in 3.14\nreportDeprecated = false\n# star import test intentionally uses `from sys import *`\nreportWildcardImportFromLibrary = false\n# some test cases intentionally exercise invalid index assignment/runtime error paths\nreportIndexIssue = false\n\n[[tool.pyright.executionEnvironments]]\nroot = \"crates/monty-type-checking\"\nreportUnnecessaryIsInstance = false\n\n[tool.codespell]\n# Ref: https://github.com/codespell-project/codespell#using-a-config-file\nignore-words-list = 'crate,NotIn,ser'\nskip = [\n    \"Cargo.lock\",\n    \"crates/monty-js/package-lock.json\",\n    \"crates/monty-typeshed/vendor/typeshed/stdlib/*\",\n    \"crates/monty/src/types/str.rs\",\n]\n\n[tool.inline-snapshot]\nformat-command=\"ruff format --stdin-filename {filename}\"\n"
  },
  {
    "path": "scripts/check_imports.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Check that all `use` statements in Rust files are at the top of the file.\n\nExits with code 1 if any misplaced imports are found.\n\"\"\"\n\nimport re\nimport sys\nfrom pathlib import Path\n\n\ndef check_file(path: Path) -> list[str]:\n    \"\"\"Check a single Rust file for misplaced imports.\n\n    Returns a list of error messages for any `use` statements found after\n    the import block (i.e., after non-import code has started).\n\n    Handles these valid patterns:\n    - mod declarations followed by pub use re-exports\n    - use statements inside mod/fn/impl blocks (tracked via brace depth)\n    \"\"\"\n    errors: list[str] = []\n    past_imports = False\n    brace_depth = 0\n\n    # Patterns that are allowed before/during the import block at top level\n    allowed_pattern = re.compile(\n        r'^(\\s*$'  # empty lines\n        r'|//[!/]'  # doc comments (//! or ///)\n        r'|//'  # regular comments\n        r'|/\\*'  # block comment start\n        r'|\\*'  # block comment continuation\n        r'|\\*/'  # block comment end\n        r'|#\\['  # attributes\n        r'|#!\\['  # inner attributes\n        r'|pub use '  # pub use statements\n        r'|pub\\(crate\\) use '  # pub(crate) use statements\n        r'|use '  # use statements\n        r'|(pub )?mod (r#)?\\w+;'  # mod declarations (mod foo; or pub mod r#type;)\n        r'|\\}'  # closing braces\n        r')'\n    )\n\n    use_pattern = re.compile(r'^use |^pub use ')\n\n    with open(path) as f:\n        for line_num, line in enumerate(f, 1):\n            stripped = line.strip()\n\n            # Track brace depth to detect when we're inside a block\n            brace_depth += line.count('{') - line.count('}')\n\n            # Only check top-level imports (brace_depth == 0)\n            if brace_depth > 0:\n                continue\n\n            # Skip allowed lines if we haven't passed the import block\n            if not past_imports and allowed_pattern.match(stripped):\n                continue\n\n            # Once we see a non-allowed line at top level, we're past imports\n            if not past_imports and stripped:\n                past_imports = True\n\n            # Check for use statements after the import block at top level\n            if past_imports and use_pattern.match(stripped):\n                errors.append(f'{path}:{line_num}: {stripped}')\n\n    return errors\n\n\ndef main() -> int:\n    \"\"\"Find all Rust files and check for misplaced imports.\"\"\"\n    all_errors: list[str] = []\n\n    for rs_file in Path('./crates').rglob('*.rs'):\n        all_errors += check_file(rs_file)\n\n    if all_errors:\n        print('Error: Found `use` statements outside the import block:', file=sys.stderr)\n        print('Move these imports to the top of the file.', file=sys.stderr)\n        print(file=sys.stderr)\n        for error in all_errors:\n            print(error, file=sys.stderr)\n        return 1\n\n    return 0\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "scripts/codecov_diff.py",
    "content": "\"\"\"\nFetch coverage diff from Codecov for a GitHub pull request.\n\nThis script uses Codecov's GraphQL API to fetch line-by-line coverage\ninformation and outputs a text file with the coverage diff.\n\nSee https://x.com/samuelcolvin/status/2019838805210198289 for rationale.\n\nUsage:\n    uv run scripts/codecov_diff.py [-h] [--org ORG] [--repo REPO] [pr-number]\n\nBy default, the org, repo and PR are auto-detected using the gh CLI.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport subprocess\nimport sys\nfrom typing import Any\n\nimport httpx\n\nCODECOV_GRAPHQL_URL = 'https://api.codecov.io/graphql/gh'\n\n# GraphQL query to get PR overview and impacted files\nPULL_QUERY = \"\"\"\nquery Pull($owner: String!, $repo: String!, $pullId: Int!) {\n  owner(username: $owner) {\n    repository(name: $repo) {\n      __typename\n      ... on Repository {\n        pull(id: $pullId) {\n          pullId\n          title\n          state\n          head {\n            commitid\n            coverageAnalytics {\n              totals {\n                percentCovered\n              }\n            }\n          }\n          compareWithBase {\n            __typename\n            ... on Comparison {\n              state\n              patchTotals {\n                percentCovered\n              }\n              headTotals {\n                percentCovered\n              }\n              changeCoverage\n              impactedFiles {\n                __typename\n                ... on ImpactedFiles {\n                  results {\n                    fileName\n                    headName\n                    missesCount\n                    patchCoverage {\n                      percentCovered\n                    }\n                    headCoverage {\n                      percentCovered\n                    }\n                    changeCoverage\n                  }\n                }\n              }\n            }\n            ... on FirstPullRequest {\n              message\n            }\n            ... on MissingBaseCommit {\n              message\n            }\n            ... on MissingHeadCommit {\n              message\n            }\n            ... on MissingComparison {\n              message\n            }\n            ... on MissingBaseReport {\n              message\n            }\n            ... on MissingHeadReport {\n              message\n            }\n          }\n        }\n      }\n      ... on NotFoundError {\n        message\n      }\n      ... on OwnerNotActivatedError {\n        message\n      }\n    }\n  }\n}\n\"\"\"\n\n# GraphQL query to get line-level coverage for a specific file\nFILE_COVERAGE_QUERY = \"\"\"\nquery ImpactedFileComparison($owner: String!, $repo: String!, $pullId: Int!, $path: String!) {\n  owner(username: $owner) {\n    repository(name: $repo) {\n      __typename\n      ... on Repository {\n        pull(id: $pullId) {\n          compareWithBase {\n            __typename\n            ... on Comparison {\n              impactedFile(path: $path) {\n                headName\n                patchCoverage {\n                  percentCovered\n                }\n                segments {\n                  __typename\n                  ... on SegmentComparisons {\n                    results {\n                      header\n                      lines {\n                        baseNumber\n                        headNumber\n                        baseCoverage\n                        headCoverage\n                        content\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\"\"\"\n\n\ndef get_repo_from_gh() -> tuple[str, str] | None:\n    \"\"\"Get the current repository's owner and name using gh CLI.\"\"\"\n    try:\n        result = subprocess.run(\n            ['gh', 'repo', 'view', '--json', 'owner,name'],\n            capture_output=True,\n            text=True,\n            check=True,\n        )\n        data = json.loads(result.stdout)\n        return data['owner']['login'], data['name']\n    except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError, FileNotFoundError):\n        return None\n\n\ndef get_pr_from_gh() -> int | None:\n    \"\"\"Get the current PR number for the current branch using gh CLI.\"\"\"\n    try:\n        result = subprocess.run(\n            ['gh', 'pr', 'view', '--json', 'number'],\n            capture_output=True,\n            text=True,\n            check=True,\n        )\n        data = json.loads(result.stdout)\n        return data['number']\n    except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError, FileNotFoundError):\n        return None\n\n\ndef graphql_request(query: str, variables: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Make a GraphQL request to Codecov API.\"\"\"\n    with httpx.Client(timeout=30.0) as client:\n        response = client.post(\n            CODECOV_GRAPHQL_URL,\n            json={'query': query, 'variables': variables},\n            headers={'Content-Type': 'application/json'},\n        )\n        response.raise_for_status()\n        return response.json()\n\n\ndef get_pull_coverage(org: str, repo: str, pr_number: int) -> dict[str, Any] | None:\n    \"\"\"Fetch PR coverage overview from Codecov.\"\"\"\n    result = graphql_request(PULL_QUERY, {'owner': org, 'repo': repo, 'pullId': pr_number})\n\n    # Navigate to the pull data\n    data = result.get('data', {})\n    owner = data.get('owner', {})\n    repository = owner.get('repository', {})\n\n    if repository.get('__typename') != 'Repository':\n        print(f'Error: {repository.get(\"message\", \"Repository not found\")}', file=sys.stderr)\n        return None\n\n    pull = repository.get('pull')\n    if not pull:\n        print('Error: Pull request not found', file=sys.stderr)\n        return None\n\n    return pull\n\n\ndef get_file_coverage(org: str, repo: str, pr_number: int, file_path: str) -> dict[str, Any] | None:\n    \"\"\"Fetch line-level coverage for a specific file.\"\"\"\n    result = graphql_request(\n        FILE_COVERAGE_QUERY,\n        {'owner': org, 'repo': repo, 'pullId': pr_number, 'path': file_path},\n    )\n\n    # Navigate to the file data\n    data = result.get('data', {})\n    owner = data.get('owner', {})\n    repository = owner.get('repository', {})\n\n    if repository.get('__typename') != 'Repository':\n        return None\n\n    pull = repository.get('pull', {})\n    compare = pull.get('compareWithBase', {})\n\n    if compare.get('__typename') != 'Comparison':\n        return None\n\n    return compare.get('impactedFile')\n\n\ndef parse_line_coverage(segments: list[dict[str, Any]]) -> tuple[list[int], list[int]]:\n    \"\"\"\n    Parse segments to extract uncovered and partial line numbers.\n\n    Coverage values from Codecov:\n    - \"H\" = hit (covered)\n    - \"M\" = miss (uncovered)\n    - \"P\" = partial\n    - null = not applicable (e.g., blank line, comment)\n    \"\"\"\n    uncovered: list[int] = []\n    partial: list[int] = []\n\n    for segment in segments:\n        lines = segment.get('lines', [])\n        for line in lines:\n            head_num = line.get('headNumber')\n            head_cov = line.get('headCoverage')\n\n            if head_num is None:\n                continue\n\n            # Convert to int since API returns strings\n            head_num = int(head_num)\n\n            if head_cov == 'M':\n                uncovered.append(head_num)\n            elif head_cov == 'P':\n                partial.append(head_num)\n\n    return sorted(set(uncovered)), sorted(set(partial))\n\n\ndef format_line_ranges(lines: list[int]) -> str:\n    \"\"\"Format a list of line numbers as ranges where consecutive.\"\"\"\n    if not lines:\n        return ''\n\n    ranges: list[str] = []\n    start = lines[0]\n    end = lines[0]\n\n    for line in lines[1:]:\n        if line == end + 1:\n            end = line\n        else:\n            if start == end:\n                ranges.append(str(start))\n            else:\n                ranges.append(f'{start}-{end}')\n            start = end = line\n\n    # Don't forget the last range\n    if start == end:\n        ranges.append(str(start))\n    else:\n        ranges.append(f'{start}-{end}')\n\n    return ', '.join(ranges)\n\n\ndef format_percentage(value: float | None) -> str:\n    \"\"\"Format a percentage value.\"\"\"\n    if value is None:\n        return 'N/A'\n    return f'{value:.2f}%'\n\n\ndef fetch_codecov_coverage(org: str, repo: str, pr_number: int) -> None:\n    \"\"\"Fetch and print coverage data from Codecov for a GitHub PR.\"\"\"\n    url = f'https://app.codecov.io/gh/{org}/{repo}/pull/{pr_number}'\n\n    pull = get_pull_coverage(org, repo, pr_number)\n    if not pull:\n        print(f'Error: Could not fetch coverage for {org}/{repo} PR #{pr_number}')\n        return\n\n    print(f'Coverage Report for {org}/{repo} PR #{pr_number}')\n    print(f'URL: {url}')\n    print(f'Title: {pull.get(\"title\", \"N/A\")}')\n    print(f'State: {pull.get(\"state\", \"N/A\")}')\n    print()\n\n    compare = pull.get('compareWithBase', {})\n    if compare.get('__typename') != 'Comparison':\n        print(f'# Note: {compare.get(\"message\", \"No comparison available\")}')\n        return\n\n    head_totals = compare.get('headTotals', {})\n    patch_totals = compare.get('patchTotals', {})\n    change = compare.get('changeCoverage')\n\n    print(f'HEAD Coverage: {format_percentage(head_totals.get(\"percentCovered\"))}')\n    print(f'Patch Coverage: {format_percentage(patch_totals.get(\"percentCovered\"))}')\n    if change is not None:\n        print(f'Change: {change:+.2f}%')\n    print()\n\n    impacted = compare.get('impactedFiles', {})\n    if impacted.get('__typename') != 'ImpactedFiles':\n        print('# No impacted files found')\n        return\n\n    for file_info in impacted.get('results', []):\n        file_path = file_info.get('headName') or file_info.get('fileName')\n        if not file_path:\n            continue\n\n        missed = file_info.get('missesCount', 0)\n        patch_cov_data = file_info.get('patchCoverage')\n        patch_cov = patch_cov_data.get('percentCovered') if patch_cov_data else None\n\n        print(f'## {file_path}')\n        if missed:\n            print(f'   Missed: {missed} lines')\n        if patch_cov is not None:\n            print(f'   Patch: {patch_cov:.2f}%')\n\n        if patch_cov is not None and patch_cov >= 100.0:\n            print('   All changed lines covered!')\n            print()\n            continue\n\n        file_data = get_file_coverage(org, repo, pr_number, file_path)\n        if file_data:\n            segments_data = file_data.get('segments', {})\n            if segments_data.get('__typename') == 'SegmentComparisons':\n                segments = segments_data.get('results', [])\n                uncovered, partial = parse_line_coverage(segments)\n\n                if uncovered:\n                    print(f'   Uncovered lines: {format_line_ranges(uncovered)}')\n                if partial:\n                    print(f'   Partial lines: {format_line_ranges(partial)}')\n\n                if not uncovered and not partial:\n                    if missed:\n                        print('   (Coverage details not available)')\n                    else:\n                        print('   All changed lines covered!')\n            else:\n                print('   (Line coverage not available)')\n        else:\n            print('   (Could not fetch line coverage)')\n\n        print()\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)\n    parser.add_argument('pr', nargs='?', type=int, help='Pull request number (auto-detected if not provided)')\n    parser.add_argument('--org', help='GitHub organization name (auto-detected if not provided)')\n    parser.add_argument('--repo', help='Repository name (auto-detected if not provided)')\n    args = parser.parse_args()\n\n    org = args.org\n    repo = args.repo\n    if not org or not repo:\n        repo_info = get_repo_from_gh()\n        if repo_info:\n            org, repo = repo_info\n        else:\n            print('Error: Could not detect repository. Use --org and --repo.', file=sys.stderr)\n            sys.exit(1)\n\n    pr_number = args.pr\n    if not pr_number:\n        pr_number = get_pr_from_gh()\n        if not pr_number:\n            print('Error: Could not detect PR number. Provide PR number as argument.', file=sys.stderr)\n            sys.exit(1)\n\n    fetch_codecov_coverage(org, repo, pr_number)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "scripts/complete_tests.py",
    "content": "\"\"\"\nComplete test expectations using CPython.\n\nScans test_cases/*.py files for incomplete expectations (e.g., `# Return=` with no value)\nand fills them in by running the code through CPython.\n\nSupported incomplete patterns:\n- `# Return=`      -> fills with repr() of result\n- `# Return.str=`  -> fills with str() of result\n- `# Return.type=` -> fills with type name of result\n- `# Raise=`       -> fills with exception type and message\n\"\"\"\n\nimport re\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef get_cpython_result(code: str, expect_type: str) -> str:\n    \"\"\"Run code through CPython and return the formatted result.\"\"\"\n    # Wrap code in a function that returns the last expression\n    lines = code.strip().split('\\n')\n    last_idx = len(lines) - 1\n\n    # Find last non-empty line\n    while last_idx >= 0 and not lines[last_idx].strip():\n        last_idx -= 1\n\n    if last_idx < 0:\n        raise ValueError('Empty code')\n\n    # Build wrapped function\n    if expect_type == 'Raise':\n        # For exceptions, don't add return\n        func_body = '\\n'.join(f'    {line}' if line.strip() else '' for line in lines[: last_idx + 1])\n    else:\n        # Add return to last line\n        func_body = '\\n'.join(f'    {line}' if line.strip() else '' for line in lines[:last_idx])\n        if func_body:\n            func_body += '\\n'\n        func_body += f'    return {lines[last_idx]}'\n\n    wrapped = f'def __test__():\\n{func_body}\\n'\n\n    # Execute and get result\n    namespace: dict[str, Any] = {}\n    exec(wrapped, namespace)\n\n    try:\n        result = namespace['__test__']()\n        if expect_type == 'Return':\n            return repr(result)\n        elif expect_type == 'Return.str':\n            return str(result)\n        elif expect_type == 'Return.type':\n            return type(result).__name__\n        elif expect_type == 'Raise':\n            raise RuntimeError('Expected exception but code completed normally')\n        else:\n            raise ValueError(f'Unknown expect_type: {expect_type}')\n    except Exception as e:\n        if expect_type == 'Raise':\n            exc_type = type(e).__name__\n            msg = str(e)\n            if not msg:\n                return f'{exc_type}()'\n            elif \"'\" in msg:\n                return f'{exc_type}(\"{msg}\")'\n            else:\n                return f\"{exc_type}('{msg}')\"\n        else:\n            raise\n\n\nincomplete_re = re.compile(r'^# (Return|Return\\.str|Return\\.type|Raise)=')\n\n\ndef process_file(filepath: Path, dry_run: bool = False) -> bool:\n    \"\"\"Process a single test file. Returns True if file was updated.\"\"\"\n    content = filepath.read_text()\n    lines = content.rstrip('\\n').split('\\n')\n\n    if not lines:\n        return False\n\n    if '# test=monty' in lines:\n        # testing only with monty, ignore\n        return False\n\n    last_line = lines[-1]\n\n    match = incomplete_re.fullmatch(last_line)\n    if not match:\n        return False\n\n    expect_type = match.group(1)\n\n    code = '\\n'.join(lines[:-1])\n    result = get_cpython_result(code, expect_type)\n\n    if result == '':\n        # happens for empty strings\n        return False\n\n    # Update the file\n    new_last_line = f'{last_line}{result}'\n    lines[-1] = new_last_line\n    new_content = '\\n'.join(lines) + '\\n'\n\n    if dry_run:\n        print(f'  Would updated {filepath.name} to assert {new_last_line!r}')\n    else:\n        filepath.write_text(new_content)\n        print(f'  Updated {filepath.name} to assert {new_last_line!r}')\n\n    return True\n\n\ndef main():\n    import argparse\n\n    parser = argparse.ArgumentParser(description='Complete test expectations using CPython')\n    parser.add_argument('--dry-run', '-n', action='store_true', help='Show what would be done without making changes')\n    parser.add_argument('files', nargs='*', help='Specific files to process (default: all test_cases/*.py)')\n    args = parser.parse_args()\n\n    # Find test files\n    script_dir = Path(__file__).parent\n    test_cases_dir = script_dir.parent / 'test_cases'\n\n    if args.files:\n        files = [Path(f) for f in args.files]\n    else:\n        files = sorted(test_cases_dir.glob('*.py'))\n\n    updated = 0\n    for filepath in files:\n        if process_file(filepath, dry_run=args.dry_run):\n            updated += 1\n\n    action = 'Would update' if args.dry_run else 'Updated'\n    print(f'\\n{action} {updated} file(s)')\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "scripts/flamegraph_to_text.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nConvert flamegraph SVGs to text format suitable for LLM reading.\n\nThis script finds all criterion flamegraph SVGs, copies them to ./flame/,\nand generates corresponding .txt files with a hierarchical text representation.\n\"\"\"\n\nimport re\nimport shutil\nimport xml.etree.ElementTree as ET\nfrom pathlib import Path\nfrom typing import TypedDict\n\n\nclass Frame(TypedDict):\n    \"\"\"A single stack frame extracted from a flamegraph SVG.\"\"\"\n\n    name: str\n    samples: int\n    percentage: float\n    y: float\n    width: int\n\n\ndef parse_title(title: str) -> tuple[str, int, float] | None:\n    \"\"\"\n    Parse a flamegraph title element.\n\n    Returns (function_name, samples, percentage) or None if parsing fails.\n    \"\"\"\n    # Match: \"function_name (N samples, X.XX%)\"\n    match = re.match(r'^(.+?)\\s+\\((\\d+)\\s+samples?,\\s+([\\d.]+)%\\)$', title)\n    if match:\n        return match.group(1), int(match.group(2)), float(match.group(3))\n    return None\n\n\ndef extract_frames(svg_path: Path) -> list[Frame]:\n    \"\"\"\n    Extract stack frames from a flamegraph SVG.\n\n    Returns a list of Frame dicts with keys: name, samples, percentage, y, width.\n    \"\"\"\n    tree = ET.parse(svg_path)\n    root = tree.getroot()\n\n    frames: list[Frame] = []\n\n    # Find all g elements (they contain title and rect)\n    for g in root.iter('{http://www.w3.org/2000/svg}g'):\n        title_elem = g.find('{http://www.w3.org/2000/svg}title')\n        rect_elem = g.find('{http://www.w3.org/2000/svg}rect')\n\n        if title_elem is None or rect_elem is None:\n            continue\n\n        title_text = title_elem.text\n        if not title_text:\n            continue\n\n        parsed = parse_title(title_text)\n        if not parsed:\n            continue\n\n        name, samples, percentage = parsed\n\n        # Get y position to determine depth (higher y = deeper in stack for bottom-up)\n        y = float(rect_elem.get('y', 0))\n        # Get width from fg:w attribute (sample count for this frame)\n        fg_w = int(rect_elem.get('{http://github.com/jonhoo/inferno}w', samples))\n\n        frames.append(\n            {\n                'name': name,\n                'samples': samples,\n                'percentage': percentage,\n                'y': y,\n                'width': fg_w,\n            }\n        )\n\n    return frames\n\n\ndef frames_to_text(frames: list[Frame], benchmark_name: str) -> str:\n    \"\"\"\n    Convert frames to a text representation suitable for LLM reading.\n\n    Produces a hierarchical view sorted by sample count (hottest first).\n    \"\"\"\n    if not frames:\n        return f'# Flamegraph: {benchmark_name}\\n\\nNo frames found.\\n'\n\n    # Sort by samples descending to show hottest functions first\n    sorted_frames = sorted(frames, key=lambda f: f['samples'], reverse=True)\n\n    # Get total samples from the root frame (highest sample count)\n    total_samples = sorted_frames[0]['samples'] if sorted_frames else 0\n\n    lines = [\n        f'# Flamegraph: {benchmark_name}',\n        f'# Total samples: {total_samples}',\n        '',\n        '## Hot Functions (sorted by sample count)',\n        '',\n    ]\n\n    # Show top functions with their percentages\n    seen: set[str] = set()\n    for frame in sorted_frames:\n        name = frame['name']\n        # Skip duplicates (same function at different stack depths)\n        if name in seen:\n            continue\n        seen.add(name)\n\n        samples = frame['samples']\n        pct = frame['percentage']\n\n        lines.append(f'{pct:6.2f}% [{samples:5d}] {name}')\n\n    # Add a section showing unique call paths\n    lines.extend(\n        [\n            '',\n            '## Stack Frames (by depth)',\n            '',\n        ]\n    )\n\n    # Group frames by y position (depth) and sort\n    by_depth: dict[float, list[Frame]] = {}\n    for frame in frames:\n        y = frame['y']\n        if y not in by_depth:\n            by_depth[y] = []\n        by_depth[y].append(frame)\n\n    # Sort depths (lower y = higher in stack for standard flamegraphs)\n    for y in sorted(by_depth.keys(), reverse=True):\n        depth_frames = sorted(by_depth[y], key=lambda f: f['samples'], reverse=True)\n        depth_num = int((max(by_depth.keys()) - y) / 16)  # Approximate depth from y\n        for frame in depth_frames[:10]:  # Limit per depth level\n            indent = '  ' * min(depth_num, 10)\n            lines.append(f'{indent}{frame[\"percentage\"]:5.2f}% {frame[\"name\"]}')\n\n    lines.append('')\n    return '\\n'.join(lines)\n\n\ndef find_flamegraph_svgs(target_dir: Path) -> list[tuple[str, Path]]:\n    \"\"\"\n    Find all criterion flamegraph SVGs.\n\n    Returns list of (benchmark_name, svg_path) tuples.\n    \"\"\"\n    criterion_dir = target_dir / 'criterion'\n    if not criterion_dir.exists():\n        return []\n\n    results: list[tuple[str, Path]] = []\n    # Pattern: target/criterion/<group>/<variant>/profile/flamegraph.svg\n    for svg_path in criterion_dir.glob('**/profile/flamegraph.svg'):\n        # Extract benchmark name from path\n        parts = svg_path.relative_to(criterion_dir).parts\n        if len(parts) >= 3:\n            benchmark_name = f'{parts[0]}_{parts[1]}'\n            results.append((benchmark_name, svg_path))\n\n    return results\n\n\ndef main():\n    project_root = Path(__file__).parent.parent\n    target_dir = project_root / 'target'\n    flame_dir = project_root / 'flame'\n\n    # Clean and recreate flame directory\n    if flame_dir.exists():\n        shutil.rmtree(flame_dir)\n    flame_dir.mkdir()\n\n    # Find all flamegraph SVGs\n    svgs = find_flamegraph_svgs(target_dir)\n\n    if not svgs:\n        print('No flamegraph SVGs found in target/criterion/')\n        print('Run `make profile` first to generate flamegraphs.')\n        return\n\n    for benchmark_name, svg_path in svgs:\n        # Copy SVG\n        dest_svg = flame_dir / f'{benchmark_name}.svg'\n        shutil.copy(svg_path, dest_svg)\n\n        # Generate text version\n        frames = extract_frames(svg_path)\n        text_content = frames_to_text(frames, benchmark_name)\n\n        dest_txt = flame_dir / f'{benchmark_name}.txt'\n        dest_txt.write_text(text_content)\n\n    print(f'\\n{len(svgs)} flamegraphs written to {flame_dir.name}/ as SVG and text files.')\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "scripts/iter_test_methods.py",
    "content": "\"\"\"\nExternal function implementations for iter mode tests.\n\nThese implementations mirror the behavior of `dispatch_external_call` in the Rust test runner\nso that iter mode tests produce identical results in both Monty and CPython.\n\nThis module is shared between:\n- scripts/run_traceback.py (for traceback tests)\n- crates/monty/tests/datatest_runner.rs (via include_str! for CPython execution)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport stat as stat_module\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\n\ndef add_ints(a: int, b: int) -> int:\n    return a + b\n\n\ndef concat_strings(a: str, b: str) -> str:\n    return a + b\n\n\ndef return_value(x: object) -> object:\n    return x\n\n\ndef get_list() -> list[int]:\n    return [1, 2, 3]\n\n\ndef raise_error(exc_type: str, message: str) -> None:\n    exc_types: dict[str, type[Exception]] = {\n        'ValueError': ValueError,\n        'TypeError': TypeError,\n        'KeyError': KeyError,\n        'RuntimeError': RuntimeError,\n    }\n    raise exc_types[exc_type](message)\n\n\n@dataclass(frozen=True)\nclass Point:\n    x: int\n    y: int\n\n    def sum(self) -> int:\n        return self.x + self.y\n\n    def add(self, dx: int, dy: int) -> 'Point':\n        return Point(x=self.x + dx, y=self.y + dy)\n\n    def scale(self, factor: int) -> 'Point':\n        return Point(x=self.x * factor, y=self.y * factor)\n\n    def describe(self, label: str = 'point') -> str:\n        return f'{label}({self.x}, {self.y})'\n\n\ndef make_point() -> Point:\n    return Point(x=1, y=2)\n\n\n@dataclass\nclass MutablePoint:\n    x: int\n    y: int\n\n    def sum(self) -> int:\n        return self.x + self.y\n\n    def shift(self, dx: int, dy: int) -> None:\n        self.x += dx\n        self.y += dy\n\n\ndef make_mutable_point() -> MutablePoint:\n    return MutablePoint(x=1, y=2)\n\n\n@dataclass(frozen=True)\nclass User:\n    name: str\n    active: bool = True\n\n    def greeting(self) -> str:\n        return f'Hello, {self.name}!'\n\n\ndef make_user(name: str) -> User:\n    return User(name=name, active=True)\n\n\n@dataclass\nclass Empty:\n    pass\n\n\ndef make_empty() -> Empty:\n    return Empty()\n\n\n# Non-function constants for NameLookup tests.\n# These mirror the values in the Rust test runner's NameLookup handler.\nCONST_INT = 42\nCONST_STR = 'hello'\nCONST_FLOAT = 3.14\nCONST_BOOL = True\nCONST_LIST = [1, 2, 3]\nCONST_NONE = None\n\n\nasync def async_call(x: object) -> object:\n    \"\"\"Async function that returns its argument.\n\n    This is a coroutine - it returns a future that resolves to the given value.\n    Used for testing async external function calls.\n    \"\"\"\n    return x\n\n\n# =============================================================================\n# Virtual Filesystem for OS Call Tests\n# =============================================================================\n\n# Virtual filesystem modification time (matches Rust constant)\nVFS_MTIME: float = 1700000000.0\n\n# Virtual files: path -> (content, mode)\nVIRTUAL_FILES: dict[str, tuple[bytes, int]] = {\n    '/virtual/file.txt': (b'hello world\\n', 0o644),\n    '/virtual/data.bin': (b'\\x00\\x01\\x02\\x03', 0o644),\n    '/virtual/empty.txt': (b'', 0o644),\n    '/virtual/subdir/nested.txt': (b'nested content', 0o644),\n    '/virtual/subdir/deep/file.txt': (b'deep', 0o644),\n    '/virtual/readonly.txt': (b'readonly', 0o444),\n}\n\n# Virtual directories\nVIRTUAL_DIRS: set[str] = {'/virtual', '/virtual/subdir', '/virtual/subdir/deep'}\n\n# Directory contents: parent_path -> list of child paths\nVIRTUAL_DIR_CONTENTS: dict[str, list[str]] = {\n    '/virtual': [\n        '/virtual/file.txt',\n        '/virtual/data.bin',\n        '/virtual/empty.txt',\n        '/virtual/subdir',\n        '/virtual/readonly.txt',\n    ],\n    '/virtual/subdir': ['/virtual/subdir/nested.txt', '/virtual/subdir/deep'],\n    '/virtual/subdir/deep': ['/virtual/subdir/deep/file.txt'],\n}\n\n\nclass VirtualStatResult:\n    \"\"\"Mock stat_result for virtual filesystem.\n\n    Mimics os.stat_result structure with named attributes and index access.\n    \"\"\"\n\n    def __init__(self, st_mode: int, st_size: int):\n        self.st_mode = st_mode\n        self.st_ino = 0\n        self.st_dev = 0\n        # nlink is 1 for files, 2 for directories\n        self.st_nlink = 1 if stat_module.S_ISREG(st_mode) else 2\n        self.st_uid = 0\n        self.st_gid = 0\n        self.st_size = st_size\n        self.st_atime = VFS_MTIME\n        self.st_mtime = VFS_MTIME\n        self.st_ctime = VFS_MTIME\n\n    def __getitem__(self, index: int) -> int | float:\n        \"\"\"Support index access like real stat_result.\"\"\"\n        fields = [\n            self.st_mode,\n            self.st_ino,\n            self.st_dev,\n            self.st_nlink,\n            self.st_uid,\n            self.st_gid,\n            self.st_size,\n            self.st_atime,\n            self.st_mtime,\n            self.st_ctime,\n        ]\n        return fields[index]\n\n\ndef is_virtual_path(path: str) -> bool:\n    \"\"\"Check if a path should use the virtual filesystem.\"\"\"\n    return path.startswith('/virtual') or path.startswith('/nonexistent')\n\n\nclass VirtualPath(type(Path())):\n    \"\"\"Path subclass that uses virtual filesystem for /virtual/ and /nonexistent paths.\n\n    Inherits from the concrete Path class (PosixPath or WindowsPath) and overrides\n    filesystem methods to use the virtual filesystem when appropriate.\n    \"\"\"\n\n    def exists(self, *, follow_symlinks: bool = True) -> bool:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            return path_str in VIRTUAL_FILES or path_str in VIRTUAL_DIRS\n        return super().exists(follow_symlinks=follow_symlinks)\n\n    def is_file(self, *, follow_symlinks: bool = True) -> bool:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            return path_str in VIRTUAL_FILES\n        return super().is_file(follow_symlinks=follow_symlinks)\n\n    def is_dir(self, *, follow_symlinks: bool = True) -> bool:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            return path_str in VIRTUAL_DIRS\n        return super().is_dir(follow_symlinks=follow_symlinks)\n\n    def is_symlink(self) -> bool:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            return False  # No symlinks in virtual fs\n        return super().is_symlink()\n\n    def read_text(self, encoding: str | None = None, errors: str | None = None, newline: str | None = None) -> str:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_FILES:\n                content, _ = VIRTUAL_FILES[path_str]\n                return content.decode('utf-8')\n            raise FileNotFoundError(2, 'No such file or directory', path_str)\n        return super().read_text(encoding=encoding, errors=errors, newline=newline)\n\n    def read_bytes(self) -> bytes:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_FILES:\n                content, _ = VIRTUAL_FILES[path_str]\n                return content\n            raise FileNotFoundError(2, 'No such file or directory', path_str)\n        return super().read_bytes()\n\n    def stat(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, *, follow_symlinks: bool = True\n    ) -> VirtualStatResult | os.stat_result:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_FILES:\n                content, mode = VIRTUAL_FILES[path_str]\n                # Add regular file type bits\n                st_mode = mode | stat_module.S_IFREG\n                return VirtualStatResult(st_mode, len(content))\n            if path_str in VIRTUAL_DIRS:\n                # Directory: 0o755 with directory type bits\n                st_mode = 0o755 | stat_module.S_IFDIR\n                return VirtualStatResult(st_mode, 4096)\n            raise FileNotFoundError(2, 'No such file or directory', path_str)\n        return super().stat(follow_symlinks=follow_symlinks)\n\n    def iterdir(self):  # pyright: ignore[reportUnknownParameterType]\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_DIR_CONTENTS:\n                for child_path in VIRTUAL_DIR_CONTENTS[path_str]:\n                    yield VirtualPath(child_path)\n                return\n            raise FileNotFoundError(2, 'No such file or directory', path_str)\n        yield from super().iterdir()\n\n    def resolve(self, strict: bool = False) -> 'VirtualPath':\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            # For virtual paths, just return as-is (already absolute)\n            return VirtualPath(path_str)\n        return VirtualPath(super().resolve(strict=strict))\n\n    def absolute(self) -> 'VirtualPath':\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            # For virtual paths, return as-is\n            return VirtualPath(path_str)\n        return VirtualPath(super().absolute())\n\n    def write_text(\n        self,\n        data: str,\n        encoding: str | None = None,\n        errors: str | None = None,\n        newline: str | None = None,\n    ) -> int:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            content = data.encode(encoding or 'utf-8')\n            VIRTUAL_FILES[path_str] = (content, 0o644)\n            # Add to parent directory contents\n            _add_to_parent_dir(path_str)\n            return len(content)\n        return super().write_text(data, encoding=encoding, errors=errors, newline=newline)\n\n    def write_bytes(self, data: bytes) -> int:  # pyright: ignore[reportIncompatibleMethodOverride]\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            VIRTUAL_FILES[path_str] = (data, 0o644)\n            # Add to parent directory contents\n            _add_to_parent_dir(path_str)\n            return len(data)\n        return super().write_bytes(data)\n\n    def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_DIRS:\n                if exist_ok:\n                    return\n                raise FileExistsError(17, 'File exists', path_str)\n            if path_str in VIRTUAL_FILES:\n                raise FileExistsError(17, 'File exists', path_str)\n\n            # Check if parent exists\n            parent_str = str(self.parent)\n            if parent_str and parent_str not in VIRTUAL_DIRS:\n                if parents:\n                    VirtualPath(parent_str).mkdir(mode=mode, parents=True, exist_ok=True)\n                else:\n                    raise FileNotFoundError(2, 'No such file or directory', path_str)\n\n            VIRTUAL_DIRS.add(path_str)\n            _add_to_parent_dir(path_str)\n            # Initialize empty directory contents\n            if path_str not in VIRTUAL_DIR_CONTENTS:\n                VIRTUAL_DIR_CONTENTS[path_str] = []\n            return\n        super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok)\n\n    def unlink(self, missing_ok: bool = False) -> None:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_FILES:\n                del VIRTUAL_FILES[path_str]\n                _remove_from_parent_dir(path_str)\n                return\n            if not missing_ok:\n                raise FileNotFoundError(2, 'No such file or directory', path_str)\n            return\n        super().unlink(missing_ok=missing_ok)\n\n    def rmdir(self) -> None:\n        path_str = str(self)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_DIRS:\n                VIRTUAL_DIRS.remove(path_str)\n                if path_str in VIRTUAL_DIR_CONTENTS:\n                    del VIRTUAL_DIR_CONTENTS[path_str]\n                _remove_from_parent_dir(path_str)\n                return\n            raise FileNotFoundError(2, 'No such file or directory', path_str)\n        super().rmdir()\n\n    def rename(self, target: 'VirtualPath | str') -> 'VirtualPath':  # pyright: ignore[reportIncompatibleMethodOverride]\n        path_str = str(self)\n        target_str = str(target)\n        if is_virtual_path(path_str):\n            if path_str in VIRTUAL_FILES:\n                content, mode = VIRTUAL_FILES[path_str]\n                del VIRTUAL_FILES[path_str]\n                _remove_from_parent_dir(path_str)\n                VIRTUAL_FILES[target_str] = (content, mode)\n                _add_to_parent_dir(target_str)\n                return VirtualPath(target_str)\n            if path_str in VIRTUAL_DIRS:\n                VIRTUAL_DIRS.remove(path_str)\n                _remove_from_parent_dir(path_str)\n                VIRTUAL_DIRS.add(target_str)\n                _add_to_parent_dir(target_str)\n                return VirtualPath(target_str)\n            raise FileNotFoundError(2, 'No such file or directory', path_str)\n        return VirtualPath(super().rename(target))\n\n    # __truediv__ is NOT overridden - the parent class already uses type(self)\n    # to create new paths, which will be VirtualPath instances\n\n\ndef _add_to_parent_dir(path_str: str) -> None:\n    \"\"\"Add a path to its parent directory's contents.\"\"\"\n    parent = str(Path(path_str).parent)\n    if parent in VIRTUAL_DIR_CONTENTS:\n        if path_str not in VIRTUAL_DIR_CONTENTS[parent]:\n            VIRTUAL_DIR_CONTENTS[parent].append(path_str)\n\n\ndef _remove_from_parent_dir(path_str: str) -> None:\n    \"\"\"Remove a path from its parent directory's contents.\"\"\"\n    parent = str(Path(path_str).parent)\n    if parent in VIRTUAL_DIR_CONTENTS and path_str in VIRTUAL_DIR_CONTENTS[parent]:\n        VIRTUAL_DIR_CONTENTS[parent].remove(path_str)\n\n\n# Monkey-patch pathlib.Path to use VirtualPath\n# This is done so tests can use `from pathlib import Path` and get VirtualPath behavior\n_original_path_new = Path.__new__\n\n\ndef _virtual_path_new(cls: type, *args: object, **kwargs: object) -> Path:\n    \"\"\"Custom __new__ that returns VirtualPath for paths starting with /virtual or /nonexistent.\n\n    Only virtual paths get the VirtualPath treatment. All other paths use the\n    standard pathlib behavior (PosixPath/WindowsPath).\n    \"\"\"\n    if cls is Path and args and isinstance(args[0], str):\n        path_str = args[0]\n        if path_str.startswith('/virtual') or path_str.startswith('/nonexistent'):\n            return object.__new__(VirtualPath)\n    return _original_path_new(cls, *args, **kwargs)  # pyright: ignore[reportUnknownVariableType,reportArgumentType]\n\n\n# Apply the monkey-patch\nPath.__new__ = _virtual_path_new\n\n\n# =============================================================================\n# Virtual Environment for os.getenv Tests\n# =============================================================================\n\n# Virtual environment variables (matches Rust test constants)\nVIRTUAL_ENV: dict[str, str] = {\n    'VIRTUAL_HOME': '/virtual/home',\n    'VIRTUAL_USER': 'testuser',\n    'VIRTUAL_EMPTY': '',\n}\n\n# Store original os functions before monkey-patching\n# Check if already patched (happens when module is re-executed in same interpreter)\nif not hasattr(os, '_monty_original_getenv'):\n    os._monty_original_getenv = os.getenv  # pyright: ignore[reportAttributeAccessIssue]\n    os._monty_original_environ = os.environ  # pyright: ignore[reportAttributeAccessIssue]\n\n_original_getenv = os._monty_original_getenv  # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType,reportUnknownMemberType]\n_original_environ = os._monty_original_environ  # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType,reportUnknownMemberType]\n\n\ndef _virtual_getenv(key: str, default: str | None = None) -> str | None:\n    \"\"\"Virtual os.getenv that returns predefined values for VIRTUAL_* keys.\n\n    For keys starting with 'VIRTUAL_', returns the virtual environment value\n    or None if not in the virtual env (ignoring default for these keys to match Monty behavior).\n    For all other keys, falls through to the real os.getenv.\n    \"\"\"\n    # Check key type first to match CPython's behavior\n    if not isinstance(key, str):  # pyright: ignore[reportUnnecessaryIsInstance]\n        # to get the real error\n        return _original_getenv(key)  # pyright: ignore[reportUnknownVariableType]\n\n    if key.startswith('VIRTUAL_') or key in ('NONEXISTENT', 'ALSO_MISSING', 'MISSING'):\n        value = VIRTUAL_ENV.get(key)\n        if value is not None:\n            return value\n        return default\n    return _original_getenv(key, default)  # pyright: ignore[reportUnknownVariableType]\n\n\n# Monkey-patch os.getenv to use virtual environment for test keys\nos.getenv = _virtual_getenv\n\n\nclass VirtualEnviron:\n    \"\"\"Wrapper around os.environ that provides virtual environment variables.\n\n    For keys in VIRTUAL_ENV or test-specific keys (NONEXISTENT, etc.), returns\n    virtual values. For all other keys, falls through to real os.environ.\n\n    This ensures tests using `os.environ['VIRTUAL_HOME']` work identically\n    in both Monty (virtual env) and CPython (real env + virtual overlay).\n    \"\"\"\n\n    def __getitem__(self, key: str) -> str:\n        if key in VIRTUAL_ENV:\n            return VIRTUAL_ENV[key]\n        if key.startswith('VIRTUAL_') or key in ('NONEXISTENT', 'ALSO_MISSING', 'MISSING'):\n            raise KeyError(key)\n        return _original_environ[key]  # pyright: ignore[reportUnknownVariableType]\n\n    def __contains__(self, key: object) -> bool:\n        if isinstance(key, str):\n            if key in VIRTUAL_ENV:\n                return True\n            if key.startswith('VIRTUAL_') or key in ('NONEXISTENT', 'ALSO_MISSING', 'MISSING'):\n                return False\n        return key in _original_environ\n\n    def __len__(self) -> int:\n        # Return only virtual env length for tests that check len(os.environ)\n        return len(VIRTUAL_ENV)\n\n    def get(self, key: str, default: str | None = None) -> str | None:\n        # Check key type first - pass through to original environ to get proper error\n        if not isinstance(key, str):  # pyright: ignore[reportUnnecessaryIsInstance]\n            return _original_environ.get(key, default)  # pyright: ignore[reportArgumentType,reportUnknownMemberType,reportUnknownVariableType]\n        if key in VIRTUAL_ENV:\n            return VIRTUAL_ENV[key]\n        if key.startswith('VIRTUAL_') or key in ('NONEXISTENT', 'ALSO_MISSING', 'MISSING'):\n            return default\n        return _original_environ.get(key, default)  # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]\n\n    def keys(self):\n        \"\"\"Return keys from virtual environment only (for test isolation).\"\"\"\n        return VIRTUAL_ENV.keys()\n\n    def values(self):\n        \"\"\"Return values from virtual environment only (for test isolation).\"\"\"\n        return VIRTUAL_ENV.values()\n\n    def items(self):\n        \"\"\"Return items from virtual environment only (for test isolation).\"\"\"\n        return VIRTUAL_ENV.items()\n\n\n# Monkey-patch os.environ to use virtual environment for test keys\nos.environ = VirtualEnviron()\n\n\n# All external functions available to iter mode tests\nITER_MODE_GLOBALS: dict[str, object] = {\n    'add_ints': add_ints,\n    'concat_strings': concat_strings,\n    'return_value': return_value,\n    'get_list': get_list,\n    'raise_error': raise_error,\n    'make_point': make_point,\n    'make_mutable_point': make_mutable_point,\n    'make_user': make_user,\n    'make_empty': make_empty,\n    'async_call': async_call,\n}\n"
  },
  {
    "path": "scripts/run_traceback.py",
    "content": "\"\"\"\nRun a Python file and return formatted traceback for testing.\n\nThis script uses runpy.run_path() to execute a file, ensuring full traceback\ninformation (including caret lines) is preserved. The file path in the traceback\nis replaced with 'test_file.py'.\n\"\"\"\n\nimport os\nimport re\nimport runpy\nimport sys\nimport tempfile\nimport traceback\nfrom threading import Lock\n\nfrom iter_test_methods import ITER_MODE_GLOBALS\n\nlock = Lock()\n\n\ndef run_file_and_get_traceback(\n    fixture_file_path: str,\n    recursion_limit: int | None = None,\n    iter_mode: bool = False,\n    async_mode: bool = False,\n) -> str | None:\n    \"\"\"\n    Execute a Python file and return the formatted traceback if an exception occurs.\n\n    The traceback will have the basename as the filename for the executed code,\n    with caret lines (`~~~~~`) properly shown for all frames.\n\n    Args:\n        fixture_file_path: Path to the Python file to execute.\n        recursion_limit: Recursion limit for execution. CPython adds ~5 frames\n            of overhead for runpy, so the effective limit for user code is\n            approximately recursion_limit - 5.\n        iter_mode: If True, inject external function implementations into globals\n            for iter mode tests (tests that use external functions like add_ints).\n        async_mode: If True, wrap code in an async context for tests with\n            top-level await that Monty supports but CPython doesn't.\n\n    Returns:\n        Formatted traceback string with '^' replaced by '~', or None if no exception.\n    \"\"\"\n    # Get absolute path for consistent replacement\n    abs_path = os.path.abspath(fixture_file_path)\n    file_name = os.path.basename(fixture_file_path)\n\n    # Async line offset: 1 lines for \"async def __test_main():\\n\"\n    line_offset = 0\n\n    with open(abs_path) as f:\n        code = f.read()\n\n    if async_mode:\n        # Wrap code in async context: indent everything by 4 spaces and add wrapper\n        indented = '\\n'.join([f'    {line}' if line else '' for line in code.split('\\n')])\n\n        code = f'async def __test_main():\\n{indented}\\nimport asyncio as __asy\\n__asy.run(__test_main())'\n        # Async line offset: 1 lines for \"async def __test_main():\\n\"\n        line_offset = 1\n\n    with lock:\n        # Set recursion limit for testing.\n        previous_recursion_limit = sys.getrecursionlimit()\n        if recursion_limit is not None:\n            sys.setrecursionlimit(recursion_limit + 5)\n\n        # Prepare init_globals for iter mode tests\n        init_globals = dict(ITER_MODE_GLOBALS) if iter_mode else None\n\n        with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as tmp_file:\n            tmp_file.write(code)\n            tmp_file.flush()\n            file_path = tmp_file.name\n\n            try:\n                runpy.run_path(file_path, init_globals=init_globals, run_name='__main__')\n            except SystemExit:\n                pass  # don't error on ctrl+c\n            except BaseException as e:\n                # Format the traceback\n                stack = traceback.format_exception(type(e), e, e.__traceback__)\n\n                result_frames: list[str] = []\n                found_user_code = False\n\n                for frame in stack:\n                    # Keep the \"Traceback (most recent call last):\" header\n                    if frame.startswith('Traceback'):\n                        result_frames.append(frame)\n                        continue\n                    elif '__asy.run(__test_main())' in frame:\n                        # Skip the asyncio.run(__test_main()) wrapper frame\n                        continue\n                    elif '/asyncio/' in frame:\n                        # Skip asyncio internal frames\n                        continue\n                    elif iter_mode:\n                        # In iter mode, skip frames from helper modules\n                        if 'iter_test_methods.py\", ' in frame:\n                            continue\n                        # python's doing something weird and show the file as <string> for dataclass exceptions\n                        if frame.startswith('  File \"<string>\"'):\n                            continue\n\n                    # Skip until we see our test file\n                    if not found_user_code and frame.startswith(f'  File \"{file_path}\"'):\n                        found_user_code = True\n\n                    if found_user_code:\n                        if async_mode:\n                            if adjusted_frame := _adjust_async_frame(frame, file_path, file_name, line_offset):\n                                result_frames.append(adjusted_frame)\n                        else:\n                            result_frames.append(frame.replace(file_path, file_name))\n\n                # Restore a high limit for traceback formatting\n                sys.setrecursionlimit(previous_recursion_limit)\n                lines = (''.join(result_frames)).splitlines()\n                return '\\n'.join(map(normalize_debug_range, lines)).rstrip()\n\n\ndef _adjust_async_frame(frame: str, tmp_path: str, file_name: str, line_offset: int) -> str | None:\n    \"\"\"\n    Adjust a traceback frame from the async wrapper to show original line numbers.\n\n    Returns the adjusted frame, or None if the frame should be skipped.\n    \"\"\"\n    # Parse the frame to extract and adjust the line number\n    # Format: '  File \"path\", line N, in func\\n    code\\n    ~~~~\\n'\n    frame = frame.replace(tmp_path, file_name)\n\n    # Replace __test_main with <module> since it represents module-level code\n    frame = frame.replace('in __test_main', 'in <module>')\n\n    # Find and adjust line number using regex\n    match = re.search(r'line (\\d+)', frame)\n    if match:\n        old_line = int(match.group(1))\n        new_line = old_line - line_offset\n        if new_line < 1:\n            return None  # Skip frames from wrapper code\n        frame = frame.replace(f'line {old_line}', f'line {new_line}')\n\n    return frame\n\n\ndef format_full_traceback(e: Exception):\n    stack = traceback.format_exception(type(e), e, e.__traceback__)\n\n    lines = (''.join(stack)).splitlines()\n    return '\\n'.join(map(normalize_debug_range, lines)).rstrip()\n\n\ndef normalize_debug_range(line: str) -> str:\n    line = line.replace('dataclasses.FrozenInstanceError:', 'FrozenInstanceError:')\n    if re.fullmatch(r' +[\\~\\^]+', line):\n        return line.replace('^', '~')\n    else:\n        return line\n\n\nif __name__ == '__main__':\n    if len(sys.argv) != 2:\n        print(f'Usage: {sys.argv[0]} <file.py>', file=sys.stderr)\n        sys.exit(1)\n\n    file_path = sys.argv[1]\n    if not os.path.exists(file_path):\n        print(f'Error: File not found: {file_path}', file=sys.stderr)\n        sys.exit(1)\n\n    result = run_file_and_get_traceback(file_path)\n    if result:\n        print(result)\n    else:\n        print('No exception raised')\n"
  },
  {
    "path": "scripts/startup_performance.py",
    "content": "# /// script\n# requires-python = \">=3.14\"\n# dependencies = [\n#     \"daytona>=0.136.0\",\n#     \"mcp-run-python>=0.0.22\",\n#     \"pydantic-monty>=0.0.1\",\n#     \"starlark-pyo3>=2025.2.5\",\n# ]\n# ///\nimport asyncio\nimport os\nimport subprocess\nimport time\nfrom typing import Any\n\nfrom mcp_run_python import code_sandbox\n\nfrom pydantic_monty import Monty\n\ncode = '1 + 1'\n\n\ndef run_monty():\n    start = time.perf_counter()\n    result = Monty('1 + 1').run()\n    diff = time.perf_counter() - start\n    assert result == 2, f'Unexpected result: {result!r}'\n    print(f'Monty cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_pyodide():\n    async def run() -> Any:\n        async with code_sandbox(dependencies=['numpy']) as sandbox:\n            return await sandbox.eval(code)\n\n    start = time.perf_counter()\n    result = asyncio.run(run())\n    diff = time.perf_counter() - start\n    assert result == {'status': 'success', 'output': [], 'return_value': 2}, f'Unexpected result: {result!r}'\n    print(f'Pyodide cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_docker():\n    start = time.perf_counter()\n    result = subprocess.run(\n        ['docker', 'run', '--rm', 'python:3.14-alpine', 'python', '-c', f'print({code})'],\n        capture_output=True,\n        text=True,\n    )\n    diff = time.perf_counter() - start\n    output = result.stdout.strip()\n    assert output == '2', f'Unexpected result: {output!r}'\n    print(f'Docker cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_starlark():\n    import starlark as sl\n\n    start = time.perf_counter()\n    glb = sl.Globals.standard()\n    mod = sl.Module()\n    ast = sl.parse('bench.star', code)\n    result = sl.eval(mod, ast, glb)\n    diff = time.perf_counter() - start\n    assert result == 2, f'Unexpected result: {result!r}'\n    print(f'Starlark cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_daytona():\n    from daytona import Daytona, DaytonaConfig\n\n    api_key = os.getenv('DAYTONA_API_KEY')\n    if not api_key:\n        print('DAYTONA_API_KEY environment variable is not set, skipping daytona')\n        return\n\n    # Initialize the Daytona client\n    daytona = Daytona(DaytonaConfig(api_key=api_key))\n\n    start = time.perf_counter()\n    response = daytona.create().process.code_run(f'print({code})')\n    diff = time.perf_counter() - start\n    assert response.result == '2', f'Unexpected result: {response.result!r}'\n    print(f'Daytona cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_wasmer():\n    # requires wasmer to be installed, see https://docs.wasmer.io/install\n    start = time.perf_counter()\n    result = subprocess.run(\n        ['wasmer', 'run', 'python/python', '--', '-c', f'print({code})'],\n        capture_output=True,\n        text=True,\n    )\n    diff = time.perf_counter() - start\n    output = result.stdout.strip()\n    assert output == '2', f'Unexpected result: {output!r}'\n    print(f'Wasmer cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_subprocess_python():\n    start = time.perf_counter()\n    result = subprocess.run(\n        ['python', '-c', f'print({code})'],\n        capture_output=True,\n        text=True,\n    )\n    diff = time.perf_counter() - start\n    output = result.stdout.strip()\n    assert output == '2', f'Unexpected result: {output!r}'\n    print(f'Subprocess Python cold start time: {(diff * 1000):.3f} milliseconds')\n\n\ndef run_exec_python():\n    start = time.perf_counter()\n    result = eval(code)\n    diff = time.perf_counter() - start\n    assert result == 2, f'Unexpected result: {result!r}'\n    print(f'Exec Python cold start time: {(diff * 1000):.3f} milliseconds')\n\n\nif __name__ == '__main__':\n    run_monty()\n    run_pyodide()\n    run_docker()\n    run_starlark()\n    run_daytona()\n    run_wasmer()\n    run_subprocess_python()\n    run_exec_python()\n"
  }
]